@keepsnappy/react-native 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +35 -35
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/{perflab/PerfLabModule.kt → keepsnappy/KeepSnappyModule.kt} +69 -28
- package/android/src/main/java/expo/modules/{perflab/PerfLabSampler.kt → keepsnappy/KeepSnappySampler.kt} +17 -17
- package/app.plugin.js +7 -6
- package/expo-module.config.json +2 -2
- package/ios/{PerfLab.podspec → KeepSnappy.podspec} +2 -2
- package/ios/{PerfLabHermes.h → KeepSnappyHermes.h} +1 -1
- package/ios/{PerfLabHermes.mm → KeepSnappyHermes.mm} +2 -2
- package/ios/{PerfLabModule.swift → KeepSnappyModule.swift} +65 -26
- package/ios/{PerfLabSampler.swift → KeepSnappySampler.swift} +10 -10
- package/js/babel-transformer.js +8 -4
- package/js/build-version.js +23 -0
- package/js/hook.polyfill.js +11 -11
- package/js/renderer-switch.js +1 -1
- package/js/sdk/activation.ts +15 -15
- package/js/sdk/active.ts +3 -3
- package/js/sdk/artifact-pending.ts +167 -0
- package/js/sdk/artifacts.ts +5 -4
- package/js/sdk/boot.ts +4 -4
- package/js/sdk/causes.ts +2 -2
- package/js/sdk/core.ts +143 -53
- package/js/sdk/cpuprof.ts +1 -1
- package/js/sdk/dormant.ts +7 -7
- package/js/sdk/links.ts +13 -13
- package/js/sdk/net.ts +2 -2
- package/js/sdk/profile-export.ts +7 -7
- package/js/sdk/push-install.ts +3 -3
- package/js/sdk/push.ts +52 -17
- package/js/sdk/sampler.ts +1 -1
- package/js/zustand-vanilla.js +2 -2
- package/metro.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Profile files a flush has not uploaded yet, kept in the app's internal storage with their run tokens (next to the push
|
|
2
|
+
// journal), so a later process uploads them: a flush that never finished, a process killed in the middle of the upload.
|
|
3
|
+
// A process's whole list is kept, uploaded files too: the run's flush report counts every file the run made.
|
|
4
|
+
|
|
5
|
+
import type { ArtifactEntry, ArtifactKind, ArtifactReason, ArtifactState } from './artifacts';
|
|
6
|
+
|
|
7
|
+
export interface PendingArtifactStore {
|
|
8
|
+
read(): string | null;
|
|
9
|
+
// '' removes the record.
|
|
10
|
+
write(content: string): boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SavedEntry {
|
|
14
|
+
name: string;
|
|
15
|
+
kind: ArtifactKind;
|
|
16
|
+
reasons: ArtifactReason[];
|
|
17
|
+
windows: number[];
|
|
18
|
+
bytes: number | null;
|
|
19
|
+
runToken: string | null;
|
|
20
|
+
state: ArtifactState;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SavedGroup {
|
|
24
|
+
processId: string;
|
|
25
|
+
// Flushes of later processes that tried this group.
|
|
26
|
+
tries: number;
|
|
27
|
+
entries: SavedEntry[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EarlierArtifacts {
|
|
31
|
+
processId: string;
|
|
32
|
+
entries: ArtifactEntry[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A file whose upload fails at this many later flushes (missing on disk, refused) stops being tried.
|
|
36
|
+
export const PENDING_ARTIFACT_TRIES = 3;
|
|
37
|
+
|
|
38
|
+
const owed = (state: ArtifactState) => state === 'pending' || state === 'failed';
|
|
39
|
+
|
|
40
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
41
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
|
+
|
|
43
|
+
const savedEntry = (value: unknown): SavedEntry | null => {
|
|
44
|
+
if (!isObject(value) || typeof value.name !== 'string' || typeof value.kind !== 'string' || typeof value.state !== 'string') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
name: value.name,
|
|
49
|
+
kind: value.kind as ArtifactKind,
|
|
50
|
+
reasons: Array.isArray(value.reasons) ? (value.reasons as ArtifactReason[]) : [],
|
|
51
|
+
windows: Array.isArray(value.windows) ? (value.windows as number[]) : [],
|
|
52
|
+
bytes: typeof value.bytes === 'number' ? value.bytes : null,
|
|
53
|
+
runToken: typeof value.runToken === 'string' ? value.runToken : null,
|
|
54
|
+
state: value.state as ArtifactState,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const parse = (raw: string | null): SavedGroup[] => {
|
|
59
|
+
if (!raw) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const value = JSON.parse(raw) as unknown;
|
|
64
|
+
if (!isObject(value) || value.v !== 1 || !Array.isArray(value.groups)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return value.groups.flatMap(group => {
|
|
68
|
+
if (!isObject(group) || typeof group.processId !== 'string' || !Array.isArray(group.entries)) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const entries = group.entries.map(savedEntry).filter((e): e is SavedEntry => e !== null);
|
|
72
|
+
return [{ processId: group.processId, tries: typeof group.tries === 'number' ? group.tries : 0, entries }];
|
|
73
|
+
});
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const toSaved = (entry: ArtifactEntry, token: string | null): SavedEntry => ({
|
|
80
|
+
name: entry.name,
|
|
81
|
+
kind: entry.kind,
|
|
82
|
+
reasons: entry.reasons,
|
|
83
|
+
windows: entry.windows,
|
|
84
|
+
bytes: entry.bytes,
|
|
85
|
+
runToken: entry.runToken !== undefined ? entry.runToken : token,
|
|
86
|
+
state: entry.state,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const toEntry = (saved: SavedEntry): ArtifactEntry => ({
|
|
90
|
+
name: saved.name,
|
|
91
|
+
kind: saved.kind,
|
|
92
|
+
reasons: saved.reasons,
|
|
93
|
+
windows: saved.windows,
|
|
94
|
+
bytes: saved.bytes,
|
|
95
|
+
state: owed(saved.state) ? 'pending' : saved.state,
|
|
96
|
+
artifactId: null,
|
|
97
|
+
gzBytes: null,
|
|
98
|
+
sha256: null,
|
|
99
|
+
attempts: 0,
|
|
100
|
+
error: null,
|
|
101
|
+
runToken: saved.runToken,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// `store` is looked up at each call: null (an older binary) keeps nothing, as before.
|
|
105
|
+
export const createPendingArtifacts = (store: () => PendingArtifactStore | null, log: (message: string) => void) => {
|
|
106
|
+
const read = () => {
|
|
107
|
+
try {
|
|
108
|
+
return parse(store()?.read() ?? null);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
log(`artifacts pending read failed ${String(error)}`);
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const save = (groups: SavedGroup[]) => {
|
|
115
|
+
const target = store();
|
|
116
|
+
if (!target) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const kept = groups.filter(group => group.entries.some(entry => owed(entry.state)));
|
|
120
|
+
try {
|
|
121
|
+
target.write(kept.length > 0 ? JSON.stringify({ v: 1, groups: kept }) : '');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
log(`artifacts pending write failed ${String(error)}`);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
// This process's files as they stand now; replaces its earlier record.
|
|
129
|
+
note(processId: string, entries: ArtifactEntry[], token: string | null) {
|
|
130
|
+
if (!store()) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const groups = read().filter(group => group.processId !== processId);
|
|
134
|
+
groups.push({ processId, tries: 0, entries: entries.map(entry => toSaved(entry, token)) });
|
|
135
|
+
save(groups);
|
|
136
|
+
},
|
|
137
|
+
// Earlier processes' lists, as entries the uploader takes: what is owed is pending again.
|
|
138
|
+
earlier(processId: string): EarlierArtifacts[] {
|
|
139
|
+
return read()
|
|
140
|
+
.filter(group => group.processId !== processId)
|
|
141
|
+
.map(group => ({ processId: group.processId, entries: group.entries.map(toEntry) }));
|
|
142
|
+
},
|
|
143
|
+
// After those were tried: what is still owed stays, for at most PENDING_ARTIFACT_TRIES flushes.
|
|
144
|
+
settleEarlier(tried: EarlierArtifacts[]) {
|
|
145
|
+
const byProcess = new Map(tried.map(group => [group.processId, group]));
|
|
146
|
+
const groups: SavedGroup[] = [];
|
|
147
|
+
for (const group of read()) {
|
|
148
|
+
const done = byProcess.get(group.processId);
|
|
149
|
+
if (!done) {
|
|
150
|
+
groups.push(group);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const entries = done.entries.map(entry => toSaved(entry, null));
|
|
154
|
+
const left = entries.filter(entry => owed(entry.state));
|
|
155
|
+
// A try is an upload that was attempted and failed; a file the deadline stopped first was not tried.
|
|
156
|
+
const attempted = done.entries.some(entry => owed(entry.state) && entry.attempts > 0);
|
|
157
|
+
const tries = attempted ? group.tries + 1 : group.tries;
|
|
158
|
+
if (left.length > 0 && tries >= PENDING_ARTIFACT_TRIES) {
|
|
159
|
+
log(`artifacts earlier dropped ${JSON.stringify({ processId: group.processId, tries, files: left.map(entry => entry.name) })}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
groups.push({ processId: group.processId, tries, entries });
|
|
163
|
+
}
|
|
164
|
+
save(groups);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
};
|
package/js/sdk/artifacts.ts
CHANGED
|
@@ -54,7 +54,7 @@ export const ARTIFACT_REQUEST_TIMEOUT_MS = 15_000;
|
|
|
54
54
|
export const ARTIFACT_PUT_TIMEOUT_MS = 60_000;
|
|
55
55
|
// One deadline per flush, started at the flush link (core.ts flushOnce) and shared by the lines push, the clock
|
|
56
56
|
// report and the artifact upload (this file's ARTIFACT_UPLOAD_DEADLINE_MS is that deadline). The end report gets its
|
|
57
|
-
// own request timeout after it, so `
|
|
57
|
+
// own request timeout after it, so `keepsnappy.flushed` is raised at most ARTIFACT_FLUSH_WORST_CASE_MS after the link.
|
|
58
58
|
// The e2e driver's wait for it is derived from this.
|
|
59
59
|
export const ARTIFACT_FLUSH_WORST_CASE_MS = ARTIFACT_UPLOAD_DEADLINE_MS + ARTIFACT_REQUEST_TIMEOUT_MS;
|
|
60
60
|
// Longest pause between two attempts at one file. SDK choice: half the Retry-After cap below, so a failing file never
|
|
@@ -324,8 +324,8 @@ export const createArtifactUploader = (deps: ArtifactUploaderDeps) => {
|
|
|
324
324
|
method: 'POST',
|
|
325
325
|
headers: {
|
|
326
326
|
'content-type': 'application/json',
|
|
327
|
-
'x-
|
|
328
|
-
'x-
|
|
327
|
+
'x-keepsnappy-key': deps.key,
|
|
328
|
+
'x-keepsnappy-run-token': token,
|
|
329
329
|
},
|
|
330
330
|
body: JSON.stringify(body),
|
|
331
331
|
...(controller ? { signal: controller.signal } : {}),
|
|
@@ -622,7 +622,8 @@ export const createArtifactUploader = (deps: ArtifactUploaderDeps) => {
|
|
|
622
622
|
deps.log(`artifacts upload stopped: ${reported}`);
|
|
623
623
|
}
|
|
624
624
|
const summary = summarizeArtifacts(entries, deps.now() - t0, reported);
|
|
625
|
-
|
|
625
|
+
// At most one request timeout past the flush deadline, however late the upload ran: ARTIFACT_FLUSH_WORST_CASE_MS holds.
|
|
626
|
+
await report(entries, token, 'end', Math.min(deps.now(), deadline) + requestTimeoutMs, reported);
|
|
626
627
|
return summary;
|
|
627
628
|
};
|
|
628
629
|
|
package/js/sdk/boot.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// keepsnappy SDK entry. withKeepSnappy (metro.js) adds this module to the graph and
|
|
2
2
|
// runs it after InitializeCore, before the app's entry module. The React hook
|
|
3
3
|
// itself was installed earlier by hook.polyfill.js (unless dormant).
|
|
4
4
|
//
|
|
5
5
|
// The measuring SDK (active.ts and everything it imports) is required only when
|
|
6
6
|
// this process measures, so a dormant process never evaluates it.
|
|
7
7
|
|
|
8
|
-
import { type Decision,
|
|
8
|
+
import { type Decision, KEEPSNAPPY_ENABLED, resolveDecision } from './activation';
|
|
9
9
|
|
|
10
|
-
if (
|
|
10
|
+
if (KEEPSNAPPY_ENABLED) {
|
|
11
11
|
try {
|
|
12
12
|
const decision: Decision = resolveDecision();
|
|
13
13
|
if (decision.active) {
|
|
@@ -16,6 +16,6 @@ if (PERF_LAB_ENABLED) {
|
|
|
16
16
|
(require('./dormant') as typeof import('./dormant')).startDormant(decision);
|
|
17
17
|
}
|
|
18
18
|
} catch (error) {
|
|
19
|
-
globalThis.console.log(`
|
|
19
|
+
globalThis.console.log(`KEEPSNAPPY boot failed ${String(error)}`);
|
|
20
20
|
}
|
|
21
21
|
}
|
package/js/sdk/causes.ts
CHANGED
|
@@ -743,7 +743,7 @@ const wrapDeviceEmitter = (emitter: { emit: AnyFn } | undefined) => {
|
|
|
743
743
|
// Responses of a request run under the cause of the code that sent it.
|
|
744
744
|
const wrapXhr = (proto: Record<string, unknown> | undefined) => {
|
|
745
745
|
if (!proto || typeof proto.send !== 'function') return;
|
|
746
|
-
const CAUSE = '
|
|
746
|
+
const CAUSE = '__keepsnappyCause';
|
|
747
747
|
const send = proto.send as AnyFn;
|
|
748
748
|
proto.send = function (this: Record<string, unknown>, ...args: unknown[]) {
|
|
749
749
|
this[CAUSE] = captured();
|
|
@@ -880,7 +880,7 @@ export const installCauses = (deps: CauseDeps = {}) => {
|
|
|
880
880
|
| { addListener?: (type: string, fn: (event: RawEvent) => void) => unknown }
|
|
881
881
|
| undefined;
|
|
882
882
|
raw?.addListener?.('*', onRawEvent);
|
|
883
|
-
g.
|
|
883
|
+
g.__KEEPSNAPPY_STORES__ = { writer: storeWriter };
|
|
884
884
|
causeStats.installed = true;
|
|
885
885
|
} catch {
|
|
886
886
|
causeStats.errors++;
|