@keyframerdev/vitals 0.1.2 → 0.1.4
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/README.md +10 -3
- package/dist/index.js +148 -16
- package/dist/types/controller.d.ts +38 -3
- package/dist/types/jsLoop.d.ts +8 -3
- package/dist/types/version.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,12 +27,15 @@ At the app's entry, with the project's field key (`kfp_live_…`, public and wri
|
|
|
27
27
|
```ts
|
|
28
28
|
import { startVitals } from '@keyframerdev/vitals'
|
|
29
29
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
30
|
-
import
|
|
30
|
+
import Constants from 'expo-constants'
|
|
31
|
+
import { Platform } from 'react-native'
|
|
32
|
+
|
|
33
|
+
const build = Platform.OS === 'ios' ? Constants.platform?.ios?.buildNumber : Constants.expoConfig?.android?.versionCode
|
|
31
34
|
|
|
32
35
|
startVitals({
|
|
33
36
|
key: 'kfp_live_…',
|
|
34
|
-
appVersion:
|
|
35
|
-
build:
|
|
37
|
+
appVersion: Constants.expoConfig?.version ?? '0',
|
|
38
|
+
build: build == null ? undefined : String(build),
|
|
36
39
|
navigationRef, // React Navigation's createNavigationContainerRef() or expo-router's useNavigationContainerRef()
|
|
37
40
|
storage: AsyncStorage, // optional: checkpoints, retries and the daily cap survive restarts
|
|
38
41
|
})
|
|
@@ -40,6 +43,10 @@ startVitals({
|
|
|
40
43
|
|
|
41
44
|
Nothing is measured or sent before `startVitals`. If your app asks for consent, call it after.
|
|
42
45
|
|
|
46
|
+
`expo-constants` rather than `expo-application`: the latter is a native module, and an EAS Update that adds it crashes every installed binary built without it. Without Expo, use `react-native-device-info` or your own constants.
|
|
47
|
+
|
|
48
|
+
Before your store release, declare it in the stores' privacy forms (App Store: Diagnostics > Performance Data; Google Play: Diagnostics) and add a sentence to your privacy policy: [what to declare](https://keyframer.dev/inspector/docs/vitals/store-privacy).
|
|
49
|
+
|
|
43
50
|
| Option | Default | What it does |
|
|
44
51
|
| --- | --- | --- |
|
|
45
52
|
| `key` | required | The project's field key |
|
package/dist/index.js
CHANGED
|
@@ -618,9 +618,13 @@ function slimArg(a, idOf) {
|
|
|
618
618
|
return null;
|
|
619
619
|
}
|
|
620
620
|
|
|
621
|
+
// ../protocol/src/vitals-validate.ts
|
|
622
|
+
var VITALS_MAX_HZ = 240;
|
|
623
|
+
var MAX_FRAME_MS = 10 * 60 * 1000;
|
|
624
|
+
|
|
621
625
|
// src/version.ts
|
|
622
626
|
var SDK_NAME = "@keyframerdev/vitals";
|
|
623
|
-
var SDK_VERSION = "0.1.
|
|
627
|
+
var SDK_VERSION = "0.1.4";
|
|
624
628
|
|
|
625
629
|
// src/controller.ts
|
|
626
630
|
var STORAGE_KEYS = {
|
|
@@ -707,6 +711,11 @@ class VitalsController {
|
|
|
707
711
|
meters = null;
|
|
708
712
|
screen = "app";
|
|
709
713
|
reduceMotion = false;
|
|
714
|
+
traced = {};
|
|
715
|
+
tracedAnims = 0;
|
|
716
|
+
metering = false;
|
|
717
|
+
drawing = true;
|
|
718
|
+
toldDevelopment = null;
|
|
710
719
|
constructor(env) {
|
|
711
720
|
this.env = env;
|
|
712
721
|
}
|
|
@@ -736,7 +745,8 @@ class VitalsController {
|
|
|
736
745
|
this.enabled = false;
|
|
737
746
|
return;
|
|
738
747
|
}
|
|
739
|
-
|
|
748
|
+
const dev = opts.environment === "development";
|
|
749
|
+
this.config = dev ? null : cached?.config ?? null;
|
|
740
750
|
if (!this.allowed()) {
|
|
741
751
|
this.enabled = false;
|
|
742
752
|
return;
|
|
@@ -753,7 +763,7 @@ class VitalsController {
|
|
|
753
763
|
return;
|
|
754
764
|
this.retryStep = 0;
|
|
755
765
|
this.scheduleRetry();
|
|
756
|
-
const fresh = cached && this.env.wall() - cached.at < Math.max(60, cached.config.ttlSec) * 1000;
|
|
766
|
+
const fresh = !dev && cached && this.env.wall() - cached.at < Math.max(60, cached.config.ttlSec) * 1000;
|
|
757
767
|
if (!fresh)
|
|
758
768
|
this.configTimer = this.env.setTimeout(() => void this.fetchConfig(run), START_DELAY_MS);
|
|
759
769
|
await this.beginSession(run);
|
|
@@ -803,6 +813,7 @@ class VitalsController {
|
|
|
803
813
|
}
|
|
804
814
|
}
|
|
805
815
|
this.sampled = sampled;
|
|
816
|
+
this.debug(`session ${this.nonce.slice(0, 6)}: ${sampled ? "sampled" : "not sampled"} (rate ${this.sampleRate}, environment ${env}` + `${devOk ? "" : ", the project does not collect development builds"})`);
|
|
806
817
|
if (!sampled)
|
|
807
818
|
return;
|
|
808
819
|
this.collector.clear();
|
|
@@ -815,6 +826,7 @@ class VitalsController {
|
|
|
815
826
|
this.meters.install();
|
|
816
827
|
}
|
|
817
828
|
endSampling() {
|
|
829
|
+
this.metering = false;
|
|
818
830
|
if (this.sampled) {
|
|
819
831
|
this.meters?.stop();
|
|
820
832
|
this.meters?.uninstall();
|
|
@@ -829,10 +841,17 @@ class VitalsController {
|
|
|
829
841
|
this.checkpointTimer = null;
|
|
830
842
|
}
|
|
831
843
|
}
|
|
832
|
-
appState(active) {
|
|
844
|
+
appState(active, drawing = active) {
|
|
833
845
|
if (!this.enabled)
|
|
834
846
|
return;
|
|
835
847
|
const now = this.env.now();
|
|
848
|
+
if (this.drawing !== drawing) {
|
|
849
|
+
this.drawing = drawing;
|
|
850
|
+
if (!drawing && this.sampled && this.foreground) {
|
|
851
|
+
this.pauseMeters(now);
|
|
852
|
+
this.debug("app inactive: meters paused until it is active again");
|
|
853
|
+
}
|
|
854
|
+
}
|
|
836
855
|
if (!active && this.foreground) {
|
|
837
856
|
this.foreground = false;
|
|
838
857
|
this.backgroundAt = now;
|
|
@@ -841,8 +860,7 @@ class VitalsController {
|
|
|
841
860
|
this.retryTimer = null;
|
|
842
861
|
}
|
|
843
862
|
if (this.sampled) {
|
|
844
|
-
this.
|
|
845
|
-
this.meters?.stop();
|
|
863
|
+
this.pauseMeters(now);
|
|
846
864
|
this.collector.foreground(false, now);
|
|
847
865
|
this.closeWindow("background");
|
|
848
866
|
}
|
|
@@ -882,6 +900,8 @@ class VitalsController {
|
|
|
882
900
|
if (isRef2(a))
|
|
883
901
|
this.collector.consume(a.$anim);
|
|
884
902
|
this.collector.animStart(site, t, ms, thread, eid);
|
|
903
|
+
if (this.opts?.debug)
|
|
904
|
+
this.tracedAnims++;
|
|
885
905
|
this.motion(t + ms, t);
|
|
886
906
|
}
|
|
887
907
|
layout(site, kind, builder, t = this.env.now()) {
|
|
@@ -905,17 +925,35 @@ class VitalsController {
|
|
|
905
925
|
if (!this.sampled || !this.foreground)
|
|
906
926
|
return;
|
|
907
927
|
this.collector.touch(down, t);
|
|
928
|
+
if (!this.drawing)
|
|
929
|
+
return;
|
|
908
930
|
if (this.gate.touch(down, t))
|
|
909
931
|
this.opened(t);
|
|
910
932
|
this.keepUi(t);
|
|
911
933
|
}
|
|
912
934
|
motion(end, now) {
|
|
935
|
+
if (!this.drawing)
|
|
936
|
+
return;
|
|
913
937
|
if (this.gate.note(end, now))
|
|
914
938
|
this.opened(now);
|
|
915
939
|
this.keepUi(now);
|
|
916
940
|
this.checkWindow(now);
|
|
917
941
|
}
|
|
942
|
+
pauseMeters(now) {
|
|
943
|
+
this.gate.reset();
|
|
944
|
+
this.meters?.stop();
|
|
945
|
+
this.collector.measure(false, now);
|
|
946
|
+
if (this.metering && this.opts?.debug) {
|
|
947
|
+
this.flushTrace();
|
|
948
|
+
this.debug("meters off: the app left the screen");
|
|
949
|
+
}
|
|
950
|
+
this.metering = false;
|
|
951
|
+
this.uiKnownUntil = -Infinity;
|
|
952
|
+
}
|
|
918
953
|
opened(now) {
|
|
954
|
+
if (!this.metering)
|
|
955
|
+
this.debug("meters on: something moves");
|
|
956
|
+
this.metering = true;
|
|
919
957
|
this.collector.measure(true, now);
|
|
920
958
|
this.meters?.start();
|
|
921
959
|
this.scheduleCheckpoint();
|
|
@@ -929,12 +967,20 @@ class VitalsController {
|
|
|
929
967
|
}
|
|
930
968
|
}
|
|
931
969
|
jsFrame(t, dt, budget) {
|
|
932
|
-
if (!this.sampled || !this.foreground)
|
|
970
|
+
if (!this.sampled || !this.foreground || !this.drawing)
|
|
933
971
|
return false;
|
|
934
|
-
if (dt > 0)
|
|
972
|
+
if (dt > 0) {
|
|
935
973
|
this.collector.frame("js", t, dt, budget);
|
|
974
|
+
if (this.opts?.debug)
|
|
975
|
+
this.traceFrame("js", dt, t);
|
|
976
|
+
}
|
|
936
977
|
if (!this.gate.open(t)) {
|
|
937
978
|
this.collector.measure(false, Math.min(t, this.gate.until));
|
|
979
|
+
if (this.metering && this.opts?.debug) {
|
|
980
|
+
this.flushTrace();
|
|
981
|
+
this.debug("meters off: the motion window closed");
|
|
982
|
+
}
|
|
983
|
+
this.metering = false;
|
|
938
984
|
return false;
|
|
939
985
|
}
|
|
940
986
|
this.keepUi(t);
|
|
@@ -946,6 +992,49 @@ class VitalsController {
|
|
|
946
992
|
return;
|
|
947
993
|
for (let i = 0;i + 2 < buf.length; i += 3)
|
|
948
994
|
this.collector.frame("ui", buf[i], buf[i + 1], buf[i + 2]);
|
|
995
|
+
if (this.opts?.debug)
|
|
996
|
+
for (let i = 0;i + 2 < buf.length; i += 3)
|
|
997
|
+
this.traceFrame("ui", buf[i + 1], buf[i]);
|
|
998
|
+
}
|
|
999
|
+
traceFrame(thread, dt, t) {
|
|
1000
|
+
const cur = this.traced[thread];
|
|
1001
|
+
if (cur && (t - cur.since >= 1000 || t - dt > cur.last + 100))
|
|
1002
|
+
this.flushTrace();
|
|
1003
|
+
const s = this.traced[thread] ??= { n: 0, sum: 0, min: Infinity, max: 0, since: t - dt, last: t };
|
|
1004
|
+
s.n++;
|
|
1005
|
+
s.sum += dt;
|
|
1006
|
+
if (dt < s.min)
|
|
1007
|
+
s.min = dt;
|
|
1008
|
+
if (dt > s.max)
|
|
1009
|
+
s.max = dt;
|
|
1010
|
+
s.since = Math.min(s.since, t - dt);
|
|
1011
|
+
s.last = Math.max(s.last, t);
|
|
1012
|
+
}
|
|
1013
|
+
flushTrace() {
|
|
1014
|
+
const all = Object.values(this.traced);
|
|
1015
|
+
if (!all.length)
|
|
1016
|
+
return;
|
|
1017
|
+
const ms = (v) => (Math.round(v * 10) / 10).toString();
|
|
1018
|
+
const part = (th) => {
|
|
1019
|
+
const s = this.traced[th];
|
|
1020
|
+
if (!s)
|
|
1021
|
+
return `${th}: no frames`;
|
|
1022
|
+
const span = Math.max(1, s.last - s.since);
|
|
1023
|
+
return `${th}: ${s.n} frames in ${Math.round(span)} ms, ${Math.round(s.n * 1000 / span)}/s (gap min ${ms(s.min)}, avg ${ms(s.sum / s.n)}, max ${ms(s.max)} ms)`;
|
|
1024
|
+
};
|
|
1025
|
+
this.debug(`on ${this.collector.currentScreen}, ${this.tracedAnims} animation starts. ${part("js")}; ${part("ui")}`);
|
|
1026
|
+
this.traced = {};
|
|
1027
|
+
this.tracedAnims = 0;
|
|
1028
|
+
}
|
|
1029
|
+
traceReport(report) {
|
|
1030
|
+
const frames = {};
|
|
1031
|
+
for (const s of report.screens)
|
|
1032
|
+
for (const [th, st] of Object.entries(s.threads))
|
|
1033
|
+
frames[th] = (frames[th] ?? 0) + (st?.frames ?? 0);
|
|
1034
|
+
const measured = report.session.measuredMs;
|
|
1035
|
+
const cap = Math.ceil(measured * VITALS_MAX_HZ / 1000);
|
|
1036
|
+
const over = Object.entries(frames).filter(([, n]) => n > cap).map(([th]) => th);
|
|
1037
|
+
this.debug(`report ${report.session.seq} (${report.session.ended}): measured ${measured} ms of ${report.session.foregroundMs} ms in the foreground, ` + `so at most about ${cap} frames a thread; js ${frames.js ?? 0}, ui ${frames.ui ?? 0}` + `${over.length ? ` (too many on ${over.join(" and ")}: the server will refuse it)` : ""}; ` + `${report.screens.length} screens, ${report.sites.length} animation sites`);
|
|
949
1038
|
}
|
|
950
1039
|
checkWindow(now) {
|
|
951
1040
|
if (this.collector.foregroundAt(now) >= VITALS_WINDOW_MS)
|
|
@@ -971,10 +1060,15 @@ class VitalsController {
|
|
|
971
1060
|
}
|
|
972
1061
|
const storage = this.opts?.storage;
|
|
973
1062
|
if (!report) {
|
|
1063
|
+
this.debug(`window closed (${ended}) with nothing to report`);
|
|
974
1064
|
await write(storage, STORAGE_KEYS.checkpoint, null);
|
|
975
1065
|
return;
|
|
976
1066
|
}
|
|
977
1067
|
this.seq++;
|
|
1068
|
+
if (this.opts?.debug) {
|
|
1069
|
+
this.flushTrace();
|
|
1070
|
+
this.traceReport(report);
|
|
1071
|
+
}
|
|
978
1072
|
await write(storage, STORAGE_KEYS.checkpoint, report);
|
|
979
1073
|
await this.send(JSON.stringify(report));
|
|
980
1074
|
await write(storage, STORAGE_KEYS.checkpoint, null);
|
|
@@ -1103,6 +1197,7 @@ class VitalsController {
|
|
|
1103
1197
|
body
|
|
1104
1198
|
});
|
|
1105
1199
|
if (res.status === 200) {
|
|
1200
|
+
this.debug("report accepted");
|
|
1106
1201
|
const json = await res.json().catch(() => null);
|
|
1107
1202
|
if (json?.config)
|
|
1108
1203
|
await this.applyConfig(json.config);
|
|
@@ -1126,13 +1221,19 @@ class VitalsController {
|
|
|
1126
1221
|
if (run !== this.run || !this.opts)
|
|
1127
1222
|
return;
|
|
1128
1223
|
try {
|
|
1129
|
-
const
|
|
1224
|
+
const fresh = Math.round(this.env.wall()).toString(36);
|
|
1225
|
+
const res = await this.env.fetch(`${this.endpoint()}/v1/config/${encodeURIComponent(this.opts.key)}?t=${fresh}`, {
|
|
1226
|
+
method: "GET",
|
|
1227
|
+
headers: { "Cache-Control": "no-cache" }
|
|
1228
|
+
});
|
|
1130
1229
|
if (run !== this.run)
|
|
1131
1230
|
return;
|
|
1132
1231
|
if (res.status === 200) {
|
|
1133
1232
|
const config = await res.json();
|
|
1134
|
-
if (config && typeof config === "object" && typeof config.sampleRate === "number")
|
|
1233
|
+
if (config && typeof config === "object" && typeof config.sampleRate === "number") {
|
|
1234
|
+
this.debug(`config: sample rate ${config.sampleRate}, development builds ${config.collect?.development ? "collected" : "not collected"}`);
|
|
1135
1235
|
await this.applyConfig(config);
|
|
1236
|
+
}
|
|
1136
1237
|
} else if (res.status === 401 || res.status === 410)
|
|
1137
1238
|
await this.killSwitch();
|
|
1138
1239
|
} catch {}
|
|
@@ -1148,6 +1249,19 @@ class VitalsController {
|
|
|
1148
1249
|
}
|
|
1149
1250
|
if (!config.collect.reduceMotion)
|
|
1150
1251
|
this.setReduceMotion(false);
|
|
1252
|
+
if (this.opts?.environment === "development")
|
|
1253
|
+
this.developmentConfig(config.collect.development);
|
|
1254
|
+
}
|
|
1255
|
+
developmentConfig(kept) {
|
|
1256
|
+
if (this.toldDevelopment !== kept) {
|
|
1257
|
+
this.toldDevelopment = kept;
|
|
1258
|
+
if (kept)
|
|
1259
|
+
this.debug("this project keeps development builds: this build's reports show in the dashboard's Development view");
|
|
1260
|
+
else
|
|
1261
|
+
this.env.log?.("[keyframer vitals] This project does not keep reports from development builds, so this build sends nothing. " + `To test here, turn on "Development builds" in the project's setup on keyframer.dev, then reload. Release builds are not affected.`, "info");
|
|
1262
|
+
}
|
|
1263
|
+
if (!kept && this.sampled)
|
|
1264
|
+
this.endSampling();
|
|
1151
1265
|
}
|
|
1152
1266
|
async killSwitch() {
|
|
1153
1267
|
this.log("turned off on this device for 7 days by the project");
|
|
@@ -1183,6 +1297,10 @@ class VitalsController {
|
|
|
1183
1297
|
log(message) {
|
|
1184
1298
|
this.env.log?.(`[keyframer vitals] ${message}`);
|
|
1185
1299
|
}
|
|
1300
|
+
debug(message) {
|
|
1301
|
+
if (this.opts?.debug)
|
|
1302
|
+
this.env.log?.(`[keyframer vitals] ${message}`, "debug");
|
|
1303
|
+
}
|
|
1186
1304
|
}
|
|
1187
1305
|
|
|
1188
1306
|
// src/hook.ts
|
|
@@ -1266,14 +1384,22 @@ function tickFieldMeter(f, t) {
|
|
|
1266
1384
|
}
|
|
1267
1385
|
|
|
1268
1386
|
// src/jsLoop.ts
|
|
1269
|
-
|
|
1387
|
+
var MIN_FRAME_GAP_MS = 1000 / 240;
|
|
1388
|
+
function createJsLoop({ raf, later, now, frame }) {
|
|
1270
1389
|
let running = false;
|
|
1271
1390
|
let gen = 0;
|
|
1391
|
+
let last = -1;
|
|
1272
1392
|
const chain = (mine) => {
|
|
1273
1393
|
const tick = () => {
|
|
1274
1394
|
if (!running || mine !== gen)
|
|
1275
1395
|
return;
|
|
1276
|
-
|
|
1396
|
+
const t = now();
|
|
1397
|
+
if (last >= 0 && t - last < MIN_FRAME_GAP_MS) {
|
|
1398
|
+
later(tick);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
last = t;
|
|
1402
|
+
if (frame(t))
|
|
1277
1403
|
raf(tick);
|
|
1278
1404
|
else
|
|
1279
1405
|
running = false;
|
|
@@ -1289,6 +1415,7 @@ function createJsLoop({ raf, frame }) {
|
|
|
1289
1415
|
return false;
|
|
1290
1416
|
running = true;
|
|
1291
1417
|
gen++;
|
|
1418
|
+
last = -1;
|
|
1292
1419
|
raf(chain(gen));
|
|
1293
1420
|
return true;
|
|
1294
1421
|
},
|
|
@@ -1323,7 +1450,7 @@ var env = {
|
|
|
1323
1450
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
1324
1451
|
clearTimeout: (h) => clearTimeout(h),
|
|
1325
1452
|
platform: { os: Platform.OS === "ios" ? "ios" : "android", osMajor: osMajor() },
|
|
1326
|
-
log: (m) => console.warn(m)
|
|
1453
|
+
log: (m, level) => level === "debug" || level === "info" ? console.log(m) : console.warn(m)
|
|
1327
1454
|
};
|
|
1328
1455
|
var controller = new VitalsController(env);
|
|
1329
1456
|
var jsHook = createJsHook(controller, () => checkScreen());
|
|
@@ -1441,8 +1568,9 @@ function extendUi(ms) {
|
|
|
1441
1568
|
var jsMeter = createFieldMeter("js");
|
|
1442
1569
|
var jsLoop = createJsLoop({
|
|
1443
1570
|
raf: (cb) => requestAnimationFrame(cb),
|
|
1444
|
-
|
|
1445
|
-
|
|
1571
|
+
later: (cb) => setTimeout(cb, 16),
|
|
1572
|
+
now,
|
|
1573
|
+
frame: (t) => {
|
|
1446
1574
|
const last = jsMeter.m.last;
|
|
1447
1575
|
const interval = jsMeter.m.interval;
|
|
1448
1576
|
tickFrameMeter(jsMeter.m, t);
|
|
@@ -1526,7 +1654,11 @@ async function startVitals(opts) {
|
|
|
1526
1654
|
lastRoute = null;
|
|
1527
1655
|
manualScreen = false;
|
|
1528
1656
|
const environment = opts.environment ?? (typeof __DEV__ !== "undefined" && __DEV__ ? "development" : "production");
|
|
1529
|
-
const appState = AppState.addEventListener("change", (s) =>
|
|
1657
|
+
const appState = AppState.addEventListener("change", (s) => {
|
|
1658
|
+
if (s !== "active")
|
|
1659
|
+
touchDown = false;
|
|
1660
|
+
controller.appState(s !== "background", s === "active");
|
|
1661
|
+
});
|
|
1530
1662
|
subscriptions.push(() => appState.remove());
|
|
1531
1663
|
if (opts.navigationRef) {
|
|
1532
1664
|
const off = opts.navigationRef.addListener("state", checkScreen);
|
|
@@ -48,6 +48,12 @@ export type ControllerOptions = {
|
|
|
48
48
|
endpoint?: string;
|
|
49
49
|
/** Bundle id or package name. */
|
|
50
50
|
appId?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Logs what this device measures and sends, with console.log: the sampling decision, when the frame
|
|
53
|
+
* meters run, frames a second per thread with their gaps, and each report against the server's
|
|
54
|
+
* limits. For development builds; leave it off in production.
|
|
55
|
+
*/
|
|
56
|
+
debug?: boolean;
|
|
51
57
|
};
|
|
52
58
|
type HttpResponse = {
|
|
53
59
|
status: number;
|
|
@@ -72,7 +78,8 @@ export type ControllerEnv = {
|
|
|
72
78
|
};
|
|
73
79
|
/** Local calendar day of a wall time, `YYYY-MM-DD`. */
|
|
74
80
|
day?(wall: number): string;
|
|
75
|
-
|
|
81
|
+
/** Warnings (refusals, kill switch), notes for the developer ('info') and, with `debug`, the trace ('debug'). */
|
|
82
|
+
log?(message: string, level?: 'warn' | 'info' | 'debug'): void;
|
|
76
83
|
};
|
|
77
84
|
/** The frame meters and the instrumentation hooks, as the platform runs them. */
|
|
78
85
|
export type Meters = {
|
|
@@ -134,6 +141,19 @@ export declare class VitalsController {
|
|
|
134
141
|
private meters;
|
|
135
142
|
private screen;
|
|
136
143
|
private reduceMotion;
|
|
144
|
+
/**
|
|
145
|
+
* debug: frames per thread, printed about once a second. Each thread keeps its own span (its earliest
|
|
146
|
+
* frame start to its latest end): UI frames reach the JS side in batches of about half a second, so a
|
|
147
|
+
* shared span would halve one thread's rate.
|
|
148
|
+
*/
|
|
149
|
+
private traced;
|
|
150
|
+
private tracedAnims;
|
|
151
|
+
/** debug: between "meters on" and "meters off", so each is logged once per motion window. */
|
|
152
|
+
private metering;
|
|
153
|
+
/** False while the app is in the foreground but not active (iOS 'inactive'): nothing is measured. */
|
|
154
|
+
private drawing;
|
|
155
|
+
/** What this launch last told the developer about the project keeping development builds. */
|
|
156
|
+
private toldDevelopment;
|
|
137
157
|
constructor(env: ControllerEnv);
|
|
138
158
|
get isSampled(): boolean;
|
|
139
159
|
get isEnabled(): boolean;
|
|
@@ -150,8 +170,11 @@ export declare class VitalsController {
|
|
|
150
170
|
stop(drop: boolean): Promise<void>;
|
|
151
171
|
private beginSession;
|
|
152
172
|
private endSampling;
|
|
153
|
-
/**
|
|
154
|
-
|
|
173
|
+
/**
|
|
174
|
+
* AppState. `active`: in the foreground (iOS 'inactive' counts, the app is still on screen). `drawing`:
|
|
175
|
+
* 'active' itself; while it is false the meters stop and motion does not start them (jsLoop.ts).
|
|
176
|
+
*/
|
|
177
|
+
appState(active: boolean, drawing?: boolean): void;
|
|
155
178
|
setScreen(name: string): void;
|
|
156
179
|
setReduceMotion(on: boolean): void;
|
|
157
180
|
/** A new id for an animation call on the JS thread (the UI runtime numbers its own from UI_EVENT_BASE). */
|
|
@@ -169,6 +192,8 @@ export declare class VitalsController {
|
|
|
169
192
|
scroll(t?: number): void;
|
|
170
193
|
touch(down: boolean, t?: number): void;
|
|
171
194
|
private motion;
|
|
195
|
+
/** Meters off and the gate shut: measured time stops at `now`; the next motion starts them again. */
|
|
196
|
+
private pauseMeters;
|
|
172
197
|
private opened;
|
|
173
198
|
/** The UI loop keeps its own time: tell it how long the gate stays open when that grows. */
|
|
174
199
|
private keepUi;
|
|
@@ -179,6 +204,10 @@ export declare class VitalsController {
|
|
|
179
204
|
jsFrame(t: number, dt: number, budget: number): boolean;
|
|
180
205
|
/** UI-thread frames as `[t, dt, budget, …]`, `t` already on the JS clock. */
|
|
181
206
|
uiFrames(buf: number[]): void;
|
|
207
|
+
private traceFrame;
|
|
208
|
+
/** One line: frames a second per thread (60, 90 or 120 when the meters are right) and their gaps. */
|
|
209
|
+
private flushTrace;
|
|
210
|
+
private traceReport;
|
|
182
211
|
private checkWindow;
|
|
183
212
|
/** The report the open window would send now, or null when it holds nothing. */
|
|
184
213
|
report(ended: VitalsReport['session']['ended'], now?: number): VitalsReport | null;
|
|
@@ -200,10 +229,16 @@ export declare class VitalsController {
|
|
|
200
229
|
private fetchConfig;
|
|
201
230
|
/** A config from the server: cached, and acted on (stop, disabled, a minimum SDK version). */
|
|
202
231
|
applyConfig(config: VitalsConfig): Promise<void>;
|
|
232
|
+
/**
|
|
233
|
+
* A development build learns whether the project keeps its reports. Off: the developer is told once a
|
|
234
|
+
* launch (the server would drop every report) and measuring stops; on: said under `debug`.
|
|
235
|
+
*/
|
|
236
|
+
private developmentConfig;
|
|
203
237
|
private killSwitch;
|
|
204
238
|
private allowed;
|
|
205
239
|
private rate;
|
|
206
240
|
private clearTimers;
|
|
207
241
|
private log;
|
|
242
|
+
private debug;
|
|
208
243
|
}
|
|
209
244
|
export {};
|
package/dist/types/jsLoop.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
/** The shortest gap between two display frames: 240 Hz, the server's limit (VITALS_MAX_HZ). */
|
|
2
|
+
export declare const MIN_FRAME_GAP_MS: number;
|
|
1
3
|
export type JsLoopDeps = {
|
|
2
4
|
raf: (cb: () => void) => unknown;
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
+
/** Waits about a frame without the display (setTimeout): used when requestAnimationFrame stops waiting for it. */
|
|
6
|
+
later: (cb: () => void) => unknown;
|
|
7
|
+
now: () => number;
|
|
8
|
+
/** One frame that ended at `t`: return false to end the loop (the motion window closed). */
|
|
9
|
+
frame: (t: number) => boolean;
|
|
5
10
|
};
|
|
6
|
-
export declare function createJsLoop({ raf, frame }: JsLoopDeps): {
|
|
11
|
+
export declare function createJsLoop({ raf, later, now, frame }: JsLoopDeps): {
|
|
7
12
|
readonly running: boolean;
|
|
8
13
|
/** Starts the loop unless it is already running. Returns true when it started a new chain. */
|
|
9
14
|
start(): boolean;
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** This package's version: sent in every report's `sdk`, and compared with the config's `minSdk`. Keep it in step with package.json. */
|
|
2
2
|
export declare const SDK_NAME = "@keyframerdev/vitals";
|
|
3
|
-
export declare const SDK_VERSION = "0.1.
|
|
3
|
+
export declare const SDK_VERSION = "0.1.4";
|
package/package.json
CHANGED