@xenon-device-management/xenon 2.9.2 → 2.10.0
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-LzfXiiZJ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-CNZJH6Cs.js → ApiKeyGate-BzFDp4mK.js} +1 -1
- package/lib/public/assets/{BugReportButton-DQ-R87ZV.js → BugReportButton-BpgWvmZo.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-dX3e7O-w.js → DeviceMosaicView-CCB-egSH.js} +2 -2
- package/lib/public/assets/{EmptyState-D3fG2GHY.js → EmptyState-DkUVmANo.js} +1 -1
- package/lib/public/assets/{FieldGroup-CUmbJZxI.js → FieldGroup-mbfnfTql.js} +1 -1
- package/lib/public/assets/{FilterMenu-Dm2c-UCk.js → FilterMenu-BH5usz4Y.js} +1 -1
- package/lib/public/assets/{Menu-Dl4uXMkH.js → Menu-Cbo9mdTk.js} +1 -1
- package/lib/public/assets/{Modal-DNmn2RDs.js → Modal-D6XfQHPr.js} +1 -1
- package/lib/public/assets/{RecordingPage-Dn3By34o.js → RecordingPage-CmFPkWqH.js} +1 -1
- package/lib/public/assets/{RecordingsPage-lCUIM4pm.js → RecordingsPage-CSlKk2qC.js} +1 -1
- package/lib/public/assets/{SegmentedControl-BdA_mHiZ.js → SegmentedControl-BFW9Dlqd.js} +1 -1
- package/lib/public/assets/{SettingCard-Dx3WhK7w.js → SettingCard-C4DioTTi.js} +1 -1
- package/lib/public/assets/{Table-D_2lhvRM.js → Table-DrKjlL00.js} +1 -1
- package/lib/public/assets/{activity-cNHHOKx5.js → activity-C9bE7Jm8.js} +1 -1
- package/lib/public/assets/{ai-settings-t6RMrDkg.js → ai-settings-DRYic4oO.js} +1 -1
- package/lib/public/assets/{api-keys-CK_qyQvW.js → api-keys-CaQU0g7c.js} +1 -1
- package/lib/public/assets/{apps-DCiI3UOb.js → apps-c1gUFmmK.js} +1 -1
- package/lib/public/assets/{arrow-left-CnYs8HVw.js → arrow-left-DMvAWA8r.js} +1 -1
- package/lib/public/assets/{arrow-right-Cc9qm6UG.js → arrow-right-BEQgxmKT.js} +1 -1
- package/lib/public/assets/{arrow-up-right-e7yBOb-c.js → arrow-up-right-Ba0e2GeU.js} +1 -1
- package/lib/public/assets/{auth-shell-DKrtcIjG.js → auth-shell-COODQnc7.js} +2 -2
- package/lib/public/assets/{builds-page-CRd5oEz6.js → builds-page-DZQgWBUs.js} +4 -4
- package/lib/public/assets/{button-C0caCjJB.js → button-WW9j43wR.js} +1 -1
- package/lib/public/assets/{calendar-BqOOkln_.js → calendar-COUs9VxF.js} +1 -1
- package/lib/public/assets/{check-DYSXRG79.js → check-D8ifHIJR.js} +1 -1
- package/lib/public/assets/{chevron-right-hCNu0CA-.js → chevron-right-BjKVUlB_.js} +1 -1
- package/lib/public/assets/{circle-check-DnQ263fa.js → circle-check-CfP79QE2.js} +1 -1
- package/lib/public/assets/{circle-x-BaRKZyz9.js → circle-x-BZbFCixZ.js} +1 -1
- package/lib/public/assets/{clock-XP3iBAhg.js → clock-BuTm35Qg.js} +1 -1
- package/lib/public/assets/{copy-D8d9UzWE.js → copy-W00zUBwN.js} +1 -1
- package/lib/public/assets/{device-explorer-pdM3L9TC.js → device-explorer-BEpS-Y-_.js} +5 -5
- package/lib/public/assets/{download-SaN9-umX.js → download-CGz-MS3y.js} +1 -1
- package/lib/public/assets/{forgot-password-DfUhEZW5.js → forgot-password-DG7SqVgU.js} +1 -1
- package/lib/public/assets/{index-C8pvxG1f.css → index-DQa193-A.css} +1 -1
- package/lib/public/assets/{index-IefGCFQQ.js → index-DWHTp1YP.js} +1 -1
- package/lib/public/assets/{index-CzaIyL9t.js → index-EkaQM1DK.js} +2 -2
- package/lib/public/assets/{input-BqFzAQT3.js → input-ChaTXeCq.js} +1 -1
- package/lib/public/assets/{keycodes-DDV2OlpQ.js → keycodes-BhbgeW_r.js} +10 -5
- package/lib/public/assets/line-chart-9P3RxScy.js +1 -0
- package/lib/public/assets/list-checks-_23IIEEL.js +11 -0
- package/lib/public/assets/{lock-Eye1CG8p.js → lock-B2JaqqRI.js} +1 -1
- package/lib/public/assets/{login-Co1IXuen.js → login-BEWIfLfC.js} +1 -1
- package/lib/public/assets/{maintenance-settings-D1fw6iqm.js → maintenance-settings-BGJIJQ-k.js} +1 -1
- package/lib/public/assets/{monitor-U5Y-PxW5.js → monitor-BrwMY4G0.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CDAwphUm.js → mouse-pointer-2-DsWDMyrY.js} +1 -1
- package/lib/public/assets/{network-BP_U4iDZ.js → network-DfIhNVJv.js} +1 -1
- package/lib/public/assets/overview-B1mePY2_.js +6 -0
- package/lib/public/assets/{page-header-ZdzNEJp9.js → page-header-DoD3TOzZ.js} +1 -1
- package/lib/public/assets/{plus-1k0sxXyU.js → plus-BGvWvpBB.js} +1 -1
- package/lib/public/assets/{profile-page-B_GWKRy3.js → profile-page-CvV5aWvl.js} +1 -1
- package/lib/public/assets/{reset-password-DizCTkdw.js → reset-password-CGVd31LR.js} +1 -1
- package/lib/public/assets/{runbook-page-DWzRiOLZ.js → runbook-page-Bq2cvwb5.js} +1 -1
- package/lib/public/assets/{select-CiRW6v17.js → select-CTcD4jWR.js} +1 -1
- package/lib/public/assets/selector-detail-redirect-19odlD4B.js +1 -0
- package/lib/public/assets/selector-health-page-LqIsLrjW.js +11 -0
- package/lib/public/assets/session-detail-page-DQzb8_fq.js +4 -0
- package/lib/public/assets/{settings-DhwZoWv7.js → settings-C-EnGIqs.js} +1 -1
- package/lib/public/assets/stat-tile-BY0RDTRm.js +1 -0
- package/lib/public/assets/{tablet-CPJV3dBq.js → tablet-B4cFM-5B.js} +1 -1
- package/lib/public/assets/{teams-D0IBa2Ov.js → teams-BFe-RpYd.js} +1 -1
- package/lib/public/assets/time-D-gjmE1o.js +1 -0
- package/lib/public/assets/{trash-2-u8MlXUBD.js → trash-2-CJCKBOP9.js} +1 -1
- package/lib/public/assets/{upload-Cq8L3kpw.js → upload-DidWkbld.js} +1 -1
- package/lib/public/assets/use-builds-data-fnfxatOd.js +8 -0
- package/lib/public/assets/{use-password-reset-mode-Dbr2HEfw.js → use-password-reset-mode-iPen3tmC.js} +1 -1
- package/lib/public/assets/{users-CZqCE511.js → users-CrcjmRdf.js} +1 -1
- package/lib/public/assets/{users-DxLHYmFm.js → users-DBUv0CJx.js} +1 -1
- package/lib/public/assets/{video-off-mh1EEzP9.js → video-off-eh69-FKB.js} +1 -1
- package/lib/public/assets/view-state-CP9dE1UI.js +1 -0
- package/lib/public/assets/{webhook-settings-BYuOib-Q.js → webhook-settings-CQoSshBn.js} +1 -1
- package/lib/public/assets/{zap-BOpUur0M.js → zap-CICtWiLn.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 +43 -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 +14 -9
- 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 +198 -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 +2 -0
- 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 +131 -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 +38 -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/schema.prisma +43 -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,94 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AndroidMetricsSampler = void 0;
|
|
13
|
+
const androidMetrics_1 = require("./androidMetrics");
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
/**
|
|
16
|
+
* Samples an Android phone every SAMPLE_INTERVAL_MS with one `adb shell`
|
|
17
|
+
* call. The app is the session's package, else the app in the foreground.
|
|
18
|
+
* Samples never overlap: the next is scheduled when one finishes.
|
|
19
|
+
*/
|
|
20
|
+
class AndroidMetricsSampler {
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
this.opts = opts;
|
|
23
|
+
this.stopped = false;
|
|
24
|
+
this.prev = null;
|
|
25
|
+
this.failures = 0;
|
|
26
|
+
this.foreground = null;
|
|
27
|
+
this.pkg = (0, androidMetrics_1.isSafePackage)(opts.appPackage) ? opts.appPackage : null;
|
|
28
|
+
}
|
|
29
|
+
start() {
|
|
30
|
+
this.schedule(0);
|
|
31
|
+
}
|
|
32
|
+
stop() {
|
|
33
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
this.stopped = true;
|
|
35
|
+
if (this.timer)
|
|
36
|
+
clearTimeout(this.timer);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
schedule(delay) {
|
|
40
|
+
var _a, _b;
|
|
41
|
+
if (this.stopped)
|
|
42
|
+
return;
|
|
43
|
+
this.timer = setTimeout(() => {
|
|
44
|
+
void this.sample().finally(() => this.schedule(types_1.SAMPLE_INTERVAL_MS));
|
|
45
|
+
}, delay);
|
|
46
|
+
(_b = (_a = this.timer).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
47
|
+
}
|
|
48
|
+
now() {
|
|
49
|
+
return this.opts.now ? this.opts.now() : Date.now();
|
|
50
|
+
}
|
|
51
|
+
sample() {
|
|
52
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
53
|
+
var _a;
|
|
54
|
+
try {
|
|
55
|
+
const appId = yield this.appId();
|
|
56
|
+
const reading = (0, androidMetrics_1.parseAndroidReading)(yield this.opts.shell((0, androidMetrics_1.androidSampleCommand)(appId)));
|
|
57
|
+
if (!reading)
|
|
58
|
+
throw new Error('unreadable /proc output');
|
|
59
|
+
const sample = (0, androidMetrics_1.androidSample)(this.prev, reading, appId, this.now());
|
|
60
|
+
this.prev = reading;
|
|
61
|
+
this.failures = 0;
|
|
62
|
+
if (!this.stopped)
|
|
63
|
+
this.opts.hooks.onSample(sample);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
this.failures += 1;
|
|
67
|
+
if (this.failures >= types_1.MAX_CONSECUTIVE_FAILURES && !this.stopped) {
|
|
68
|
+
this.stopped = true;
|
|
69
|
+
this.opts.hooks.onGiveUp((_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
appId() {
|
|
75
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
76
|
+
if (this.pkg)
|
|
77
|
+
return this.pkg;
|
|
78
|
+
const now = this.now();
|
|
79
|
+
if (this.foreground && now - this.foreground.at < types_1.FOREGROUND_REFRESH_MS) {
|
|
80
|
+
return this.foreground.pkg;
|
|
81
|
+
}
|
|
82
|
+
let pkg = null;
|
|
83
|
+
try {
|
|
84
|
+
pkg = (0, androidMetrics_1.parseForegroundPackage)(yield this.opts.shell('dumpsys activity activities'));
|
|
85
|
+
}
|
|
86
|
+
catch (_a) {
|
|
87
|
+
// Device figures still count; the app waits for the next lookup.
|
|
88
|
+
}
|
|
89
|
+
this.foreground = { pkg, at: now };
|
|
90
|
+
return pkg;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
exports.AndroidMetricsSampler = AndroidMetricsSampler;
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.IOSMetricsSampler = void 0;
|
|
13
|
+
const iosMetrics_1 = require("./iosMetrics");
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
/** A reading older than this is not reported again. */
|
|
16
|
+
const STALE_MS = 2 * types_1.SAMPLE_INTERVAL_MS;
|
|
17
|
+
/**
|
|
18
|
+
* Device CPU of an iPhone, from a long-running `ios sysmontap` through the
|
|
19
|
+
* phone's tunnel (borrowed, and asked for again so it outlives the 2-minute
|
|
20
|
+
* idle stop). Reports the latest reading every SAMPLE_INTERVAL_MS.
|
|
21
|
+
*/
|
|
22
|
+
class IOSMetricsSampler {
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.opts = opts;
|
|
25
|
+
this.stopped = false;
|
|
26
|
+
this.failures = 0;
|
|
27
|
+
this.timers = [];
|
|
28
|
+
}
|
|
29
|
+
start() {
|
|
30
|
+
void this.launch();
|
|
31
|
+
this.every(types_1.SAMPLE_INTERVAL_MS, () => this.report());
|
|
32
|
+
this.every(types_1.TUNNEL_RENEW_MS, () => {
|
|
33
|
+
void this.opts.tunnels.borrow(this.opts.udid).catch(() => null);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
stop() {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
var _a;
|
|
39
|
+
this.stopped = true;
|
|
40
|
+
this.timers.forEach((t) => clearInterval(t));
|
|
41
|
+
if (this.relaunch)
|
|
42
|
+
clearTimeout(this.relaunch);
|
|
43
|
+
(_a = this.proc) === null || _a === void 0 ? void 0 : _a.kill('SIGTERM');
|
|
44
|
+
this.proc = undefined;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
every(ms, fn) {
|
|
48
|
+
var _a;
|
|
49
|
+
const t = setInterval(fn, ms);
|
|
50
|
+
(_a = t.unref) === null || _a === void 0 ? void 0 : _a.call(t);
|
|
51
|
+
this.timers.push(t);
|
|
52
|
+
}
|
|
53
|
+
launch() {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
if (this.stopped)
|
|
57
|
+
return;
|
|
58
|
+
// Below iOS 17 there is no tunnel (null), and sysmontap needs none.
|
|
59
|
+
yield this.opts.tunnels.borrow(this.opts.udid).catch(() => null);
|
|
60
|
+
if (this.stopped)
|
|
61
|
+
return;
|
|
62
|
+
const proc = this.opts.spawnSysmontap(this.opts.tunnels.envFor(this.opts.udid));
|
|
63
|
+
this.proc = proc;
|
|
64
|
+
let rest = '';
|
|
65
|
+
const read = (chunk) => {
|
|
66
|
+
var _a;
|
|
67
|
+
const lines = (rest + chunk.toString()).split('\n');
|
|
68
|
+
rest = (_a = lines.pop()) !== null && _a !== void 0 ? _a : '';
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const cpu = (0, iosMetrics_1.parseSysmontapCpu)(line);
|
|
71
|
+
if (cpu !== null) {
|
|
72
|
+
this.latest = { cpu, at: Date.now() };
|
|
73
|
+
this.failures = 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
(_a = proc.stdout) === null || _a === void 0 ? void 0 : _a.on('data', read);
|
|
78
|
+
(_b = proc.stderr) === null || _b === void 0 ? void 0 : _b.on('data', read);
|
|
79
|
+
let ended = false;
|
|
80
|
+
const end = () => {
|
|
81
|
+
if (ended)
|
|
82
|
+
return;
|
|
83
|
+
ended = true;
|
|
84
|
+
this.exited(proc);
|
|
85
|
+
};
|
|
86
|
+
proc.once('exit', end);
|
|
87
|
+
proc.once('error', end);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
exited(proc) {
|
|
91
|
+
var _a, _b;
|
|
92
|
+
if (this.proc === proc)
|
|
93
|
+
this.proc = undefined;
|
|
94
|
+
if (this.stopped)
|
|
95
|
+
return;
|
|
96
|
+
this.failures += 1;
|
|
97
|
+
if (this.failures >= types_1.MAX_CONSECUTIVE_FAILURES) {
|
|
98
|
+
void this.stop();
|
|
99
|
+
this.opts.hooks.onGiveUp('ios sysmontap kept exiting');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const delay = Math.min(30000, types_1.SAMPLE_INTERVAL_MS * 2 ** (this.failures - 1));
|
|
103
|
+
this.relaunch = setTimeout(() => void this.launch(), delay);
|
|
104
|
+
(_b = (_a = this.relaunch).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
105
|
+
}
|
|
106
|
+
report() {
|
|
107
|
+
if (this.stopped || !this.latest || Date.now() - this.latest.at > STALE_MS)
|
|
108
|
+
return;
|
|
109
|
+
this.opts.hooks.onSample({
|
|
110
|
+
at: Date.now(),
|
|
111
|
+
deviceCpuPct: this.latest.cpu,
|
|
112
|
+
deviceMemMb: null,
|
|
113
|
+
deviceMemTotalMb: null,
|
|
114
|
+
appCpuPct: null,
|
|
115
|
+
appMemMb: null,
|
|
116
|
+
appId: null,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.IOSMetricsSampler = IOSMetricsSampler;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
9
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
10
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
11
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
12
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
13
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
14
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.SessionMetricsService = void 0;
|
|
22
|
+
const child_process_1 = require("child_process");
|
|
23
|
+
const util_1 = require("util");
|
|
24
|
+
const typedi_1 = require("typedi");
|
|
25
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
26
|
+
const prisma_1 = require("../../prisma");
|
|
27
|
+
const PluginContext_1 = require("../../PluginContext");
|
|
28
|
+
const localDeviceHosts_1 = require("../../device-managers/localDeviceHosts");
|
|
29
|
+
const device_managers_1 = require("../../device-managers");
|
|
30
|
+
const AndroidDeviceManager_1 = __importDefault(require("../../device-managers/AndroidDeviceManager"));
|
|
31
|
+
const IOSTunnels_1 = require("../../device-managers/ios/IOSTunnels");
|
|
32
|
+
const goIosBinary_1 = require("../../device-managers/ios/goIosBinary");
|
|
33
|
+
const ProcessRegistry_1 = require("../ProcessRegistry");
|
|
34
|
+
const androidMetrics_1 = require("./androidMetrics");
|
|
35
|
+
const AndroidMetricsSampler_1 = require("./AndroidMetricsSampler");
|
|
36
|
+
const IOSMetricsSampler_1 = require("./IOSMetricsSampler");
|
|
37
|
+
const types_1 = require("./types");
|
|
38
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
39
|
+
/** One adb call of a sample; one that hangs this long is a failed sample. */
|
|
40
|
+
const ADB_TIMEOUT_MS = 5000;
|
|
41
|
+
/**
|
|
42
|
+
* CPU and memory for the session page's Performance panel. A sampler per
|
|
43
|
+
* session on this server's own phones, from EventManager's start (once the
|
|
44
|
+
* session's row exists) to its stop; samples are written every
|
|
45
|
+
* FLUSH_INTERVAL_MS. Sampling never fails a session.
|
|
46
|
+
*/
|
|
47
|
+
let SessionMetricsService = class SessionMetricsService {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.log = logger_1.default.scope('SessionMetrics');
|
|
50
|
+
this.running = new Map();
|
|
51
|
+
}
|
|
52
|
+
/** Whether a session on this phone is sampled. */
|
|
53
|
+
appliesTo(device) {
|
|
54
|
+
var _a, _b;
|
|
55
|
+
if (!device)
|
|
56
|
+
return false;
|
|
57
|
+
const ctx = this.context();
|
|
58
|
+
if (((_a = ctx.pluginArgs) === null || _a === void 0 ? void 0 : _a.sessionMetrics) === false)
|
|
59
|
+
return false;
|
|
60
|
+
const platform = String((_b = device.platform) !== null && _b !== void 0 ? _b : '').toLowerCase();
|
|
61
|
+
// An emulator's /proc reads like a phone's; a simulator has no sysmontap.
|
|
62
|
+
if (platform !== 'android' && !(platform === 'ios' && device.realDevice === true)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return (0, localDeviceHosts_1.isOwnDevice)((0, localDeviceHosts_1.localDeviceHosts)(ctx.pluginArgs, ctx.port), ctx.nodeId, device);
|
|
66
|
+
}
|
|
67
|
+
start({ sessionId, device, capabilities }) {
|
|
68
|
+
var _a, _b;
|
|
69
|
+
if (this.running.has(sessionId) || !this.appliesTo(device))
|
|
70
|
+
return;
|
|
71
|
+
const hooks = {
|
|
72
|
+
onSample: (s) => { var _a; return (_a = this.running.get(sessionId)) === null || _a === void 0 ? void 0 : _a.buffer.push(s); },
|
|
73
|
+
onGiveUp: (reason) => {
|
|
74
|
+
const entry = this.running.get(sessionId);
|
|
75
|
+
if (entry)
|
|
76
|
+
entry.gaveUp = true;
|
|
77
|
+
this.log.warn(`[${sessionId}] Stopped sampling ${device.udid}: ${reason}`);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
let sampler;
|
|
81
|
+
try {
|
|
82
|
+
sampler = this.samplerFor(sessionId, device, capabilities, hooks);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
this.log.warn(`[${sessionId}] Can't sample ${device.udid}: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const flushTimer = setInterval(() => void this.flush(sessionId), types_1.FLUSH_INTERVAL_MS);
|
|
89
|
+
(_b = flushTimer.unref) === null || _b === void 0 ? void 0 : _b.call(flushTimer);
|
|
90
|
+
this.running.set(sessionId, {
|
|
91
|
+
sampler,
|
|
92
|
+
buffer: [],
|
|
93
|
+
flushTimer,
|
|
94
|
+
writing: Promise.resolve(),
|
|
95
|
+
gaveUp: false,
|
|
96
|
+
});
|
|
97
|
+
sampler.start();
|
|
98
|
+
this.log.info(`[${sessionId}] Sampling CPU and memory on ${device.udid}`);
|
|
99
|
+
}
|
|
100
|
+
/** Whether this server is sampling a running session, stopped after giving up, or never did. */
|
|
101
|
+
recordingState(sessionId) {
|
|
102
|
+
const entry = this.running.get(sessionId);
|
|
103
|
+
if (!entry)
|
|
104
|
+
return 'off';
|
|
105
|
+
return entry.gaveUp ? 'stopped' : 'sampling';
|
|
106
|
+
}
|
|
107
|
+
/** Stops the session's sampler and writes what it buffered. Idempotent. */
|
|
108
|
+
stop(sessionId) {
|
|
109
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
const entry = this.running.get(sessionId);
|
|
111
|
+
if (!entry)
|
|
112
|
+
return;
|
|
113
|
+
this.running.delete(sessionId);
|
|
114
|
+
clearInterval(entry.flushTimer);
|
|
115
|
+
yield entry.sampler.stop().catch(() => undefined);
|
|
116
|
+
yield this.write(sessionId, entry);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
flush(sessionId) {
|
|
120
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
121
|
+
const entry = this.running.get(sessionId);
|
|
122
|
+
if (entry)
|
|
123
|
+
yield this.write(sessionId, entry);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** On failure the samples wait for the next write; the newest MAX_BUFFERED_SAMPLES are kept. */
|
|
127
|
+
write(sessionId, entry) {
|
|
128
|
+
entry.writing = entry.writing.then(() => __awaiter(this, void 0, void 0, function* () {
|
|
129
|
+
var _a;
|
|
130
|
+
const batch = entry.buffer.splice(0);
|
|
131
|
+
if (batch.length === 0)
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
yield this.writeSamples(sessionId, batch);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
entry.buffer.unshift(...batch);
|
|
138
|
+
const over = entry.buffer.length - types_1.MAX_BUFFERED_SAMPLES;
|
|
139
|
+
if (over > 0)
|
|
140
|
+
entry.buffer.splice(0, over);
|
|
141
|
+
this.log.debug(`[${sessionId}] Writing ${batch.length} samples failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
return entry.writing;
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------
|
|
147
|
+
// Seams. Overridden by tests; the defaults are the real thing.
|
|
148
|
+
// ---------------------------------------------------------------------
|
|
149
|
+
context() {
|
|
150
|
+
return typedi_1.Container.get(PluginContext_1.PluginContext);
|
|
151
|
+
}
|
|
152
|
+
writeSamples(sessionId, samples) {
|
|
153
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
154
|
+
yield prisma_1.prisma.sessionMetric.createMany({
|
|
155
|
+
data: samples.map((s) => ({
|
|
156
|
+
session_id: sessionId,
|
|
157
|
+
at: s.at,
|
|
158
|
+
device_cpu_pct: s.deviceCpuPct,
|
|
159
|
+
device_mem_mb: s.deviceMemMb,
|
|
160
|
+
device_mem_total: s.deviceMemTotalMb,
|
|
161
|
+
app_cpu_pct: s.appCpuPct,
|
|
162
|
+
app_mem_mb: s.appMemMb,
|
|
163
|
+
app_id: s.appId,
|
|
164
|
+
})),
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
samplerFor(sessionId, device, capabilities, hooks) {
|
|
169
|
+
if (String(device.platform).toLowerCase() === 'android') {
|
|
170
|
+
let adb;
|
|
171
|
+
return new AndroidMetricsSampler_1.AndroidMetricsSampler({
|
|
172
|
+
appPackage: (0, androidMetrics_1.appPackageOf)(capabilities),
|
|
173
|
+
hooks,
|
|
174
|
+
shell: (command) => __awaiter(this, void 0, void 0, function* () {
|
|
175
|
+
if (!adb)
|
|
176
|
+
adb = this.adbCommand(device.udid);
|
|
177
|
+
try {
|
|
178
|
+
const { path, base } = yield adb;
|
|
179
|
+
return yield this.execAdb(path, [...base, 'shell', command]);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
adb = undefined; // looked up again next time
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return new IOSMetricsSampler_1.IOSMetricsSampler({
|
|
189
|
+
udid: device.udid,
|
|
190
|
+
tunnels: typedi_1.Container.get(IOSTunnels_1.IOSTunnels),
|
|
191
|
+
hooks,
|
|
192
|
+
spawnSysmontap: (env) => {
|
|
193
|
+
const proc = (0, child_process_1.spawn)((0, goIosBinary_1.goIosBinaryPath)(), ['sysmontap', '--udid', device.udid], {
|
|
194
|
+
env,
|
|
195
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
196
|
+
});
|
|
197
|
+
typedi_1.Container.get(ProcessRegistry_1.ProcessRegistry).track({
|
|
198
|
+
sessionId,
|
|
199
|
+
udid: device.udid,
|
|
200
|
+
kind: 'other',
|
|
201
|
+
process: proc,
|
|
202
|
+
});
|
|
203
|
+
return proc;
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* The resolved adb, and the arguments that reach this phone: the host's,
|
|
209
|
+
* then `-s <udid>`. The shared appium-adb instance names no device (its
|
|
210
|
+
* `shell()` adds `-s` only after `setDeviceId`), so with several phones on
|
|
211
|
+
* one adb server a plain `adb shell` fails.
|
|
212
|
+
*/
|
|
213
|
+
adbCommand(udid) {
|
|
214
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
215
|
+
var _a;
|
|
216
|
+
const managers = yield typedi_1.Container.get(device_managers_1.XenonManager).deviceInstances();
|
|
217
|
+
const android = managers.find((m) => m instanceof AndroidDeviceManager_1.default);
|
|
218
|
+
if (!android)
|
|
219
|
+
throw new Error('no Android device manager');
|
|
220
|
+
const adb = (yield android.getAdbForDevice(udid));
|
|
221
|
+
const path = (_a = adb === null || adb === void 0 ? void 0 : adb.executable) === null || _a === void 0 ? void 0 : _a.path;
|
|
222
|
+
if (!path)
|
|
223
|
+
throw new Error(`adb executable path not resolved for ${udid}`);
|
|
224
|
+
const hostArgs = adb.adbHost && adb.adbPort ? ['-H', adb.adbHost, '-P', String(adb.adbPort)] : [];
|
|
225
|
+
return { path, base: [...hostArgs, '-s', udid] };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
execAdb(path, args) {
|
|
229
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
230
|
+
const { stdout } = yield execFileAsync(path, args, { timeout: ADB_TIMEOUT_MS });
|
|
231
|
+
return stdout;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
exports.SessionMetricsService = SessionMetricsService;
|
|
236
|
+
exports.SessionMetricsService = SessionMetricsService = __decorate([
|
|
237
|
+
(0, typedi_1.Service)()
|
|
238
|
+
], SessionMetricsService);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSafePackage = isSafePackage;
|
|
4
|
+
exports.appPackageOf = appPackageOf;
|
|
5
|
+
exports.androidSampleCommand = androidSampleCommand;
|
|
6
|
+
exports.parseAndroidReading = parseAndroidReading;
|
|
7
|
+
exports.androidSample = androidSample;
|
|
8
|
+
exports.parseForegroundPackage = parseForegroundPackage;
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
/**
|
|
11
|
+
* An Android package name: two or more dot-separated identifiers. A session's
|
|
12
|
+
* appPackage is client input and goes into a shell command, so nothing else
|
|
13
|
+
* is ever used.
|
|
14
|
+
*/
|
|
15
|
+
function isSafePackage(pkg) {
|
|
16
|
+
return typeof pkg === 'string' && /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/.test(pkg);
|
|
17
|
+
}
|
|
18
|
+
/** A session's appPackage capability, in either spelling. Not yet validated. */
|
|
19
|
+
function appPackageOf(caps) {
|
|
20
|
+
var _a;
|
|
21
|
+
return (_a = caps === null || caps === void 0 ? void 0 : caps['appium:appPackage']) !== null && _a !== void 0 ? _a : caps === null || caps === void 0 ? void 0 : caps.appPackage;
|
|
22
|
+
}
|
|
23
|
+
/** The one shell command a sample runs: device CPU and memory, then the app's. */
|
|
24
|
+
function androidSampleCommand(pkg) {
|
|
25
|
+
const device = 'head -1 /proc/stat; grep -e ^MemTotal: -e ^MemAvailable: /proc/meminfo';
|
|
26
|
+
if (pkg === null)
|
|
27
|
+
return device;
|
|
28
|
+
if (!isSafePackage(pkg))
|
|
29
|
+
throw new Error(`not a package name: ${pkg}`);
|
|
30
|
+
return (`${device}; p=$(pidof ${pkg} | cut -d' ' -f1); echo "pid=$p"; ` +
|
|
31
|
+
'if [ -n "$p" ]; then echo "stat=$(cut -d" " -f14,15 /proc/$p/stat)"; grep VmRSS /proc/$p/status; fi');
|
|
32
|
+
}
|
|
33
|
+
const num = (m) => (m ? Number(m[1]) : null);
|
|
34
|
+
function parseAndroidReading(out) {
|
|
35
|
+
var _a;
|
|
36
|
+
const cpu = out.match(/^cpu\s+([\d\s]+)$/m);
|
|
37
|
+
if (!cpu)
|
|
38
|
+
return null;
|
|
39
|
+
const f = cpu[1].trim().split(/\s+/).map(Number);
|
|
40
|
+
if (f.length < 4 || f.some((n) => !Number.isFinite(n)))
|
|
41
|
+
return null;
|
|
42
|
+
// user nice system idle iowait irq softirq steal; guest time is already in user.
|
|
43
|
+
const cpuTotal = f.slice(0, 8).reduce((a, b) => a + b, 0);
|
|
44
|
+
const cpuIdle = f[3] + ((_a = f[4]) !== null && _a !== void 0 ? _a : 0);
|
|
45
|
+
const pid = num(out.match(/^pid=(\d+)\s*$/m));
|
|
46
|
+
const stat = out.match(/^stat=(\d+) (\d+)\s*$/m);
|
|
47
|
+
return {
|
|
48
|
+
cpuTotal,
|
|
49
|
+
cpuIdle,
|
|
50
|
+
memTotalKb: num(out.match(/^MemTotal:\s+(\d+) kB/m)),
|
|
51
|
+
memAvailableKb: num(out.match(/^MemAvailable:\s+(\d+) kB/m)),
|
|
52
|
+
pid,
|
|
53
|
+
appJiffies: pid !== null && stat ? Number(stat[1]) + Number(stat[2]) : null,
|
|
54
|
+
appRssKb: pid !== null ? num(out.match(/^VmRSS:\s+(\d+) kB/m)) : null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const pct = (share) => (0, types_1.round1)((0, types_1.clamp)(share * 100, 0, 100));
|
|
58
|
+
const mb = (kb) => (0, types_1.round1)(kb / 1024);
|
|
59
|
+
/**
|
|
60
|
+
* A sample from this reading and the one before it. CPU needs both: the
|
|
61
|
+
* first sample has none, and the app's needs the same process in both, so a
|
|
62
|
+
* restarted app shows no CPU for one sample rather than a spike.
|
|
63
|
+
*/
|
|
64
|
+
function androidSample(prev, cur, appId, at) {
|
|
65
|
+
let deviceCpuPct = null;
|
|
66
|
+
let appCpuPct = null;
|
|
67
|
+
const dTotal = prev ? cur.cpuTotal - prev.cpuTotal : 0;
|
|
68
|
+
if (prev && dTotal > 0) {
|
|
69
|
+
deviceCpuPct = pct(1 - (cur.cpuIdle - prev.cpuIdle) / dTotal);
|
|
70
|
+
if (cur.pid !== null &&
|
|
71
|
+
cur.pid === prev.pid &&
|
|
72
|
+
cur.appJiffies !== null &&
|
|
73
|
+
prev.appJiffies !== null) {
|
|
74
|
+
appCpuPct = pct((cur.appJiffies - prev.appJiffies) / dTotal);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const hasApp = cur.pid !== null;
|
|
78
|
+
return {
|
|
79
|
+
at,
|
|
80
|
+
deviceCpuPct,
|
|
81
|
+
deviceMemMb: cur.memTotalKb !== null && cur.memAvailableKb !== null
|
|
82
|
+
? mb(cur.memTotalKb - cur.memAvailableKb)
|
|
83
|
+
: null,
|
|
84
|
+
deviceMemTotalMb: cur.memTotalKb !== null ? mb(cur.memTotalKb) : null,
|
|
85
|
+
appCpuPct,
|
|
86
|
+
appMemMb: hasApp && cur.appRssKb !== null ? mb(cur.appRssKb) : null,
|
|
87
|
+
appId: hasApp ? appId : null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** The foreground app from `dumpsys activity activities`, before and after Android 10. */
|
|
91
|
+
function parseForegroundPackage(dumpsys) {
|
|
92
|
+
const m = dumpsys.match(/(?:topResumedActivity|mResumedActivity)[:=]\s*ActivityRecord\{\S+ u\d+ ([^\s/]+)\//);
|
|
93
|
+
return m && isSafePackage(m[1]) ? m[1] : null;
|
|
94
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSysmontapCpu = parseSysmontapCpu;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
/**
|
|
6
|
+
* Device CPU % from one line of `ios sysmontap`, or null for any other line.
|
|
7
|
+
* go-ios logs JSON lines; a reading is {"msg":"received CPU usage data",
|
|
8
|
+
* "enabled_cpus":6,"cpu_total_load":77.5,...}. The load is summed over the
|
|
9
|
+
* cores (an idle 6-core iPhone 14 Plus read 77 to 125), so it is divided by
|
|
10
|
+
* them.
|
|
11
|
+
*/
|
|
12
|
+
function parseSysmontapCpu(line) {
|
|
13
|
+
var _a;
|
|
14
|
+
let o;
|
|
15
|
+
try {
|
|
16
|
+
o = JSON.parse(line);
|
|
17
|
+
}
|
|
18
|
+
catch (_b) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (!o || typeof o.cpu_total_load !== 'number' || !Number.isFinite(o.cpu_total_load)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const cores = (_a = [o.enabled_cpus, o.cpu_count].find((n) => typeof n === 'number' && n > 0)) !== null && _a !== void 0 ? _a : 1;
|
|
25
|
+
return (0, types_1.round1)((0, types_1.clamp)(o.cpu_total_load / cores, 0, 100));
|
|
26
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.seriesFor = seriesFor;
|
|
4
|
+
exports.sessionMetricsBody = sessionMetricsBody;
|
|
5
|
+
const types_1 = require("./types");
|
|
6
|
+
/** What a session on this platform can have: Android everything, an iPhone device CPU. */
|
|
7
|
+
function seriesFor(platform) {
|
|
8
|
+
const p = platform.toLowerCase();
|
|
9
|
+
const android = p === 'android';
|
|
10
|
+
return {
|
|
11
|
+
deviceCpu: android || p === 'ios',
|
|
12
|
+
deviceMem: android,
|
|
13
|
+
appCpu: android,
|
|
14
|
+
appMem: android,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function sessionMetricsBody(platform, rows, recording = null) {
|
|
18
|
+
var _a;
|
|
19
|
+
const sorted = [...rows].sort((a, b) => a.at - b.at);
|
|
20
|
+
const lastApp = [...sorted].reverse().find((r) => r.app_id);
|
|
21
|
+
return {
|
|
22
|
+
platform: platform.toLowerCase(),
|
|
23
|
+
intervalMs: types_1.SAMPLE_INTERVAL_MS,
|
|
24
|
+
appId: (_a = lastApp === null || lastApp === void 0 ? void 0 : lastApp.app_id) !== null && _a !== void 0 ? _a : null,
|
|
25
|
+
series: seriesFor(platform),
|
|
26
|
+
recording,
|
|
27
|
+
samples: sorted.map((r) => ({
|
|
28
|
+
t: r.at,
|
|
29
|
+
deviceCpu: r.device_cpu_pct,
|
|
30
|
+
deviceMemMb: r.device_mem_mb,
|
|
31
|
+
deviceMemTotalMb: r.device_mem_total,
|
|
32
|
+
appCpu: r.app_cpu_pct,
|
|
33
|
+
appMemMb: r.app_mem_mb,
|
|
34
|
+
app: r.app_id,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clamp = exports.round1 = exports.MAX_BUFFERED_SAMPLES = exports.TUNNEL_RENEW_MS = exports.FOREGROUND_REFRESH_MS = exports.MAX_CONSECUTIVE_FAILURES = exports.FLUSH_INTERVAL_MS = exports.SAMPLE_INTERVAL_MS = void 0;
|
|
4
|
+
/** How often a session's phone is sampled. */
|
|
5
|
+
exports.SAMPLE_INTERVAL_MS = 2000;
|
|
6
|
+
/** How often a session's samples are written; a crash loses at most this much. */
|
|
7
|
+
exports.FLUSH_INTERVAL_MS = 10000;
|
|
8
|
+
/** A sampler that fails this many times in a row stops for its session. */
|
|
9
|
+
exports.MAX_CONSECUTIVE_FAILURES = 5;
|
|
10
|
+
/** How long a looked-up foreground app is trusted (Android, no appPackage). */
|
|
11
|
+
exports.FOREGROUND_REFRESH_MS = 10000;
|
|
12
|
+
/** How often an iPhone sampler asks for its phone's tunnel again. */
|
|
13
|
+
exports.TUNNEL_RENEW_MS = 30000;
|
|
14
|
+
/** Samples waiting for a write that keeps failing: the newest 30 minutes. */
|
|
15
|
+
exports.MAX_BUFFERED_SAMPLES = 900;
|
|
16
|
+
const round1 = (n) => Math.round(n * 10) / 10;
|
|
17
|
+
exports.round1 = round1;
|
|
18
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
19
|
+
exports.clamp = clamp;
|