react-native-nitro-tracing 0.6.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/NitroTracing.podspec +32 -0
- package/README.md +75 -0
- package/android/CMakeLists.txt +49 -0
- package/android/build.gradle +133 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/proguard-rules.pro +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +47 -0
- package/android/src/main/java/com/margelo/nitro/nitrotracing/HermesSamplingGuard.kt +43 -0
- package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingFrameSource.kt +36 -0
- package/android/src/main/java/com/margelo/nitro/nitrotracing/NitroTracingPackage.kt +31 -0
- package/client.ts +15 -0
- package/cpp/HermesSampling.cpp +26 -0
- package/cpp/HermesSampling.hpp +10 -0
- package/cpp/HybridRecording.cpp +66 -0
- package/cpp/HybridRecording.hpp +30 -0
- package/cpp/HybridTraceSpan.cpp +10 -0
- package/cpp/HybridTraceSpan.hpp +17 -0
- package/cpp/HybridTracing.cpp +10 -0
- package/cpp/HybridTracing.hpp +10 -0
- package/cpp/NativeConversions.cpp +77 -0
- package/cpp/NativeConversions.hpp +15 -0
- package/cpp/core/NativeSampler.cpp +152 -0
- package/cpp/core/NativeSampler.hpp +56 -0
- package/cpp/core/Recorder.cpp +218 -0
- package/cpp/core/Recorder.hpp +60 -0
- package/cpp/core/Retention.cpp +26 -0
- package/cpp/core/Retention.hpp +7 -0
- package/cpp/core/TraceJson.cpp +214 -0
- package/cpp/core/TraceJson.hpp +7 -0
- package/cpp/core/TraceTypes.hpp +62 -0
- package/expo.ts +2 -0
- package/ios/NitroTracingFrameSource.mm +35 -0
- package/lib/commonjs/client.cjs +767 -0
- package/lib/commonjs/client.cjs.map +7 -0
- package/lib/commonjs/expo.cjs +88 -0
- package/lib/commonjs/expo.cjs.map +7 -0
- package/lib/commonjs/index.cjs +28 -0
- package/lib/commonjs/index.cjs.map +7 -0
- package/lib/commonjs/performance.cjs +112 -0
- package/lib/commonjs/performance.cjs.map +7 -0
- package/lib/commonjs/plugins.cjs +477 -0
- package/lib/commonjs/plugins.cjs.map +7 -0
- package/lib/commonjs/react.cjs +3833 -0
- package/lib/commonjs/react.cjs.map +7 -0
- package/lib/commonjs/release-profiler.cjs +142 -0
- package/lib/commonjs/release-profiler.cjs.map +7 -0
- package/lib/commonjs/sentry.cjs +188 -0
- package/lib/commonjs/sentry.cjs.map +7 -0
- package/lib/module/client.mjs +768 -0
- package/lib/module/client.mjs.map +7 -0
- package/lib/module/expo.mjs +62 -0
- package/lib/module/expo.mjs.map +7 -0
- package/lib/module/index.mjs +7 -0
- package/lib/module/index.mjs.map +7 -0
- package/lib/module/performance.mjs +89 -0
- package/lib/module/performance.mjs.map +7 -0
- package/lib/module/plugins.mjs +461 -0
- package/lib/module/plugins.mjs.map +7 -0
- package/lib/module/react.mjs +3851 -0
- package/lib/module/react.mjs.map +7 -0
- package/lib/module/release-profiler.mjs +137 -0
- package/lib/module/release-profiler.mjs.map +7 -0
- package/lib/module/sentry.mjs +165 -0
- package/lib/module/sentry.mjs.map +7 -0
- package/lib/typescript/client.d.ts +4 -0
- package/lib/typescript/expo.d.ts +2 -0
- package/lib/typescript/performance.d.ts +2 -0
- package/lib/typescript/plugins.d.ts +12 -0
- package/lib/typescript/react.d.ts +5 -0
- package/lib/typescript/release-profiler.d.ts +2 -0
- package/lib/typescript/sentry.d.ts +2 -0
- package/lib/typescript/src/client/TracingSink.d.ts +10 -0
- package/lib/typescript/src/client/createTraceClient.d.ts +82 -0
- package/lib/typescript/src/client/playground.d.ts +12 -0
- package/lib/typescript/src/expo/ExpoAdapter.d.ts +15 -0
- package/lib/typescript/src/expo/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/plugins/TracePlugin.d.ts +22 -0
- package/lib/typescript/src/plugins/errors.d.ts +15 -0
- package/lib/typescript/src/plugins/nativeMetrics.d.ts +13 -0
- package/lib/typescript/src/plugins/navigation.d.ts +29 -0
- package/lib/typescript/src/plugins/network.d.ts +23 -0
- package/lib/typescript/src/plugins/orderSpans.d.ts +3 -0
- package/lib/typescript/src/plugins/performance.d.ts +13 -0
- package/lib/typescript/src/plugins/releaseProfiler.d.ts +25 -0
- package/lib/typescript/src/plugins/runtimeMetrics.d.ts +17 -0
- package/lib/typescript/src/plugins/sentry.d.ts +15 -0
- package/lib/typescript/src/plugins/startPlugins.d.ts +4 -0
- package/lib/typescript/src/react/DetailView.d.ts +11 -0
- package/lib/typescript/src/react/ExplorerFilters.d.ts +9 -0
- package/lib/typescript/src/react/ExplorerView.d.ts +9 -0
- package/lib/typescript/src/react/InspectorControls.d.ts +229 -0
- package/lib/typescript/src/react/InspectorHeader.d.ts +8 -0
- package/lib/typescript/src/react/InspectorSheet.d.ts +16 -0
- package/lib/typescript/src/react/MetricsView.d.ts +7 -0
- package/lib/typescript/src/react/SummaryView.d.ts +11 -0
- package/lib/typescript/src/react/TimelineView.d.ts +8 -0
- package/lib/typescript/src/react/TraceInspector.d.ts +11 -0
- package/lib/typescript/src/react/TraceOverlay.d.ts +17 -0
- package/lib/typescript/src/react/explorerModel.d.ts +38 -0
- package/lib/typescript/src/react/labels.d.ts +203 -0
- package/lib/typescript/src/react/native.d.ts +42 -0
- package/lib/typescript/src/react/readSnapshot.d.ts +4 -0
- package/lib/typescript/src/react/store.d.ts +9 -0
- package/lib/typescript/src/react/topics.d.ts +80 -0
- package/lib/typescript/src/react/useAppReadyMetric.d.ts +3 -0
- package/lib/typescript/src/react/useLiveRecording.d.ts +4 -0
- package/lib/typescript/src/react/useTraceViewer.d.ts +140 -0
- package/lib/typescript/src/react/viewerModel.d.ts +59 -0
- package/lib/typescript/src/specs/Recording.nitro.d.ts +31 -0
- package/lib/typescript/src/specs/TraceSpan.nitro.d.ts +14 -0
- package/lib/typescript/src/specs/Tracing.nitro.d.ts +17 -0
- package/lib/typescript/src/types/CompletedSpanOptions.d.ts +15 -0
- package/lib/typescript/src/types/EventContext.d.ts +8 -0
- package/lib/typescript/src/types/MarkEvent.d.ts +4 -0
- package/lib/typescript/src/types/MarkOptions.d.ts +6 -0
- package/lib/typescript/src/types/MetricEvent.d.ts +8 -0
- package/lib/typescript/src/types/MetricOptions.d.ts +10 -0
- package/lib/typescript/src/types/NativeSamplingOptions.d.ts +7 -0
- package/lib/typescript/src/types/ReadOptions.d.ts +7 -0
- package/lib/typescript/src/types/RecordingOptions.d.ts +11 -0
- package/lib/typescript/src/types/RecordingStats.d.ts +25 -0
- package/lib/typescript/src/types/SpanEvent.d.ts +13 -0
- package/lib/typescript/src/types/SpanOptions.d.ts +6 -0
- package/lib/typescript/src/types/SpanOutcome.d.ts +2 -0
- package/lib/typescript/src/types/TraceAttribute.d.ts +7 -0
- package/lib/typescript/src/types/TraceAttributes.d.ts +3 -0
- package/lib/typescript/src/types/TraceContext.d.ts +10 -0
- package/lib/typescript/src/types/TracePage.d.ts +18 -0
- package/lib/typescript/src/types.d.ts +17 -0
- package/nitro.json +20 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroTracing+autolinking.cmake +83 -0
- package/nitrogen/generated/android/NitroTracing+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroTracingOnLoad.cpp +49 -0
- package/nitrogen/generated/android/NitroTracingOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrotracing/NitroTracingOnLoad.kt +35 -0
- package/nitrogen/generated/ios/NitroTracing+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Bridge.hpp +27 -0
- package/nitrogen/generated/ios/NitroTracing-Swift-Cxx-Umbrella.hpp +38 -0
- package/nitrogen/generated/ios/NitroTracingAutolinking.mm +35 -0
- package/nitrogen/generated/ios/NitroTracingAutolinking.swift +16 -0
- package/nitrogen/generated/shared/c++/CompletedSpanOptions.hpp +122 -0
- package/nitrogen/generated/shared/c++/HybridRecordingSpec.cpp +31 -0
- package/nitrogen/generated/shared/c++/HybridRecordingSpec.hpp +100 -0
- package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.cpp +23 -0
- package/nitrogen/generated/shared/c++/HybridTraceSpanSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/HybridTracingSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridTracingSpec.hpp +68 -0
- package/nitrogen/generated/shared/c++/MarkEvent.hpp +102 -0
- package/nitrogen/generated/shared/c++/MarkOptions.hpp +99 -0
- package/nitrogen/generated/shared/c++/MetricEvent.hpp +110 -0
- package/nitrogen/generated/shared/c++/MetricOptions.hpp +107 -0
- package/nitrogen/generated/shared/c++/NativeSamplingOptions.hpp +87 -0
- package/nitrogen/generated/shared/c++/ReadOptions.hpp +87 -0
- package/nitrogen/generated/shared/c++/RecordingOptions.hpp +95 -0
- package/nitrogen/generated/shared/c++/RecordingStats.hpp +123 -0
- package/nitrogen/generated/shared/c++/SpanEvent.hpp +121 -0
- package/nitrogen/generated/shared/c++/SpanOptions.hpp +99 -0
- package/nitrogen/generated/shared/c++/SpanOutcome.hpp +84 -0
- package/nitrogen/generated/shared/c++/TraceAttribute.hpp +87 -0
- package/nitrogen/generated/shared/c++/TracePage.hpp +111 -0
- package/package.json +236 -0
- package/performance.ts +2 -0
- package/plugins.ts +24 -0
- package/react-native.config.js +16 -0
- package/react.ts +10 -0
- package/release-profiler.ts +6 -0
- package/sentry.ts +2 -0
- package/src/client/TracingSink.ts +25 -0
- package/src/client/createTraceClient.ts +439 -0
- package/src/client/playground.ts +143 -0
- package/src/expo/ExpoAdapter.ts +71 -0
- package/src/expo/index.ts +2 -0
- package/src/index.ts +8 -0
- package/src/plugins/TracePlugin.ts +22 -0
- package/src/plugins/errors.ts +69 -0
- package/src/plugins/nativeMetrics.ts +26 -0
- package/src/plugins/navigation.ts +111 -0
- package/src/plugins/network.ts +178 -0
- package/src/plugins/orderSpans.ts +22 -0
- package/src/plugins/performance.ts +120 -0
- package/src/plugins/releaseProfiler.ts +123 -0
- package/src/plugins/runtimeMetrics.ts +131 -0
- package/src/plugins/sentry.ts +176 -0
- package/src/plugins/startPlugins.ts +58 -0
- package/src/react/DetailView.tsx +514 -0
- package/src/react/ExplorerFilters.tsx +151 -0
- package/src/react/ExplorerView.tsx +243 -0
- package/src/react/InspectorControls.tsx +651 -0
- package/src/react/InspectorHeader.tsx +171 -0
- package/src/react/InspectorSheet.tsx +89 -0
- package/src/react/MetricsView.tsx +182 -0
- package/src/react/SummaryView.tsx +121 -0
- package/src/react/TimelineView.tsx +87 -0
- package/src/react/TraceInspector.tsx +196 -0
- package/src/react/TraceOverlay.tsx +585 -0
- package/src/react/explorerModel.ts +285 -0
- package/src/react/labels.ts +234 -0
- package/src/react/native.tsx +159 -0
- package/src/react/readSnapshot.ts +58 -0
- package/src/react/store.ts +31 -0
- package/src/react/topics.ts +427 -0
- package/src/react/useAppReadyMetric.ts +10 -0
- package/src/react/useLiveRecording.ts +43 -0
- package/src/react/useTraceViewer.ts +333 -0
- package/src/react/viewerModel.ts +172 -0
- package/src/specs/Recording.nitro.ts +40 -0
- package/src/specs/TraceSpan.nitro.ts +14 -0
- package/src/specs/Tracing.nitro.ts +14 -0
- package/src/types/CompletedSpanOptions.ts +15 -0
- package/src/types/EventContext.ts +8 -0
- package/src/types/MarkEvent.ts +3 -0
- package/src/types/MarkOptions.ts +6 -0
- package/src/types/MetricEvent.ts +8 -0
- package/src/types/MetricOptions.ts +10 -0
- package/src/types/NativeSamplingOptions.ts +7 -0
- package/src/types/ReadOptions.ts +7 -0
- package/src/types/RecordingOptions.ts +11 -0
- package/src/types/RecordingStats.ts +25 -0
- package/src/types/SpanEvent.ts +13 -0
- package/src/types/SpanOptions.ts +6 -0
- package/src/types/SpanOutcome.ts +2 -0
- package/src/types/TraceAttribute.ts +7 -0
- package/src/types/TraceAttributes.ts +4 -0
- package/src/types/TraceContext.ts +10 -0
- package/src/types/TracePage.ts +18 -0
- package/src/types.ts +17 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntimeMetricsPlugin,
|
|
3
|
+
type RuntimeMetricsOptions,
|
|
4
|
+
} from '../plugins/runtimeMetrics'
|
|
5
|
+
import {
|
|
6
|
+
formatTracingEntry,
|
|
7
|
+
type TracingEntry,
|
|
8
|
+
type TracingSink,
|
|
9
|
+
} from './TracingSink'
|
|
10
|
+
import type { Recording } from '../specs/Recording.nitro'
|
|
11
|
+
import type { RecordingOptions } from '../types'
|
|
12
|
+
import type { PerformancePluginOptions } from '../plugins/performance'
|
|
13
|
+
import { createPerformancePlugin } from '../plugins/performance'
|
|
14
|
+
import { startPlugins } from '../plugins/startPlugins'
|
|
15
|
+
import type { PluginHandle, TracePlugin } from '../plugins/TracePlugin'
|
|
16
|
+
import type { ReleaseProfilerPlugin } from '../plugins/releaseProfiler'
|
|
17
|
+
import { recordPlayground, type PlaygroundReport } from './playground'
|
|
18
|
+
|
|
19
|
+
/** `recording`: lossless native schema. `perfetto`: Chrome Trace Events for ui.perfetto.dev. */
|
|
20
|
+
export type TraceFormat = 'recording' | 'perfetto'
|
|
21
|
+
export type ClientAttributes = Record<
|
|
22
|
+
string,
|
|
23
|
+
string | number | boolean | undefined
|
|
24
|
+
>
|
|
25
|
+
export interface TraceClientOptions {
|
|
26
|
+
/** Optional app log/breadcrumb output; failures never affect measured work. */
|
|
27
|
+
sink?: TracingSink
|
|
28
|
+
plugins?: TracePlugin[]
|
|
29
|
+
performance?: false | PerformancePluginOptions
|
|
30
|
+
/** Opt in to foreground JS event-loop and frame-callback metrics. */
|
|
31
|
+
runtimeMetrics?: boolean | RuntimeMetricsOptions
|
|
32
|
+
onError?: (error: unknown) => void
|
|
33
|
+
recordingOptions?: Partial<RecordingOptions>
|
|
34
|
+
/** Serialize and deliver a recording; `format` picks raw recording JSON or Perfetto trace events. */
|
|
35
|
+
share?: (recording: Recording, format: TraceFormat) => Promise<void>
|
|
36
|
+
shareProfile?: (path: string) => Promise<void>
|
|
37
|
+
/** Start Hermes sampling with each recording; stopped and saved with it. */
|
|
38
|
+
autoProfile?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* CPU profile window captured by `flag()` when a profiler is attached and idle.
|
|
41
|
+
* Default 10000 ms; 0 records only the flag mark.
|
|
42
|
+
*/
|
|
43
|
+
flagProfileMs?: number
|
|
44
|
+
profiler?: ReleaseProfilerPlugin
|
|
45
|
+
}
|
|
46
|
+
export interface TraceClientSnapshot {
|
|
47
|
+
visible: boolean
|
|
48
|
+
recording: boolean
|
|
49
|
+
busy: boolean
|
|
50
|
+
disposed: boolean
|
|
51
|
+
error?: string
|
|
52
|
+
profiling: boolean
|
|
53
|
+
profilePath?: string
|
|
54
|
+
playground?: PlaygroundReport
|
|
55
|
+
/** Plugin IDs started for the current recording, so the UI can tell untracked from quiet. */
|
|
56
|
+
collectors?: string[]
|
|
57
|
+
}
|
|
58
|
+
/** react-native-performance when installed (existing marks live in its buffer), else RN's global Web Performance API. */
|
|
59
|
+
const defaultPerformanceApi = (): PerformancePluginOptions | undefined => {
|
|
60
|
+
try {
|
|
61
|
+
// Directly inside `try` so Metro treats the optional peer as optional.
|
|
62
|
+
const module =
|
|
63
|
+
require('react-native-performance') as typeof import('react-native-performance')
|
|
64
|
+
return {
|
|
65
|
+
performance: module.default,
|
|
66
|
+
PerformanceObserver: module.PerformanceObserver,
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
const global = globalThis as unknown as Partial<PerformancePluginOptions>
|
|
70
|
+
return global.performance && global.PerformanceObserver
|
|
71
|
+
? (global as PerformancePluginOptions)
|
|
72
|
+
: undefined
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Each client exclusively owns its recording and producers. Consumers must dispose it. */
|
|
76
|
+
export function createTraceClient(options: TraceClientOptions = {}) {
|
|
77
|
+
let recording: Recording | undefined
|
|
78
|
+
let handle: PluginHandle | undefined
|
|
79
|
+
const readinessStartMs = globalThis.performance.now()
|
|
80
|
+
let readyReported = false
|
|
81
|
+
let enabled = false
|
|
82
|
+
let disposed = false
|
|
83
|
+
let pending = 0
|
|
84
|
+
let tail = Promise.resolve()
|
|
85
|
+
let snapshot: TraceClientSnapshot = {
|
|
86
|
+
visible: false,
|
|
87
|
+
recording: false,
|
|
88
|
+
busy: false,
|
|
89
|
+
disposed: false,
|
|
90
|
+
profiling: false,
|
|
91
|
+
}
|
|
92
|
+
const listeners = new Set<() => void>()
|
|
93
|
+
const marks = new Map<
|
|
94
|
+
string,
|
|
95
|
+
{ marks: Map<string, number>; emitted: Set<string> }
|
|
96
|
+
>()
|
|
97
|
+
const publish = (next: Partial<TraceClientSnapshot> = {}) => {
|
|
98
|
+
snapshot = {
|
|
99
|
+
...snapshot,
|
|
100
|
+
recording: enabled,
|
|
101
|
+
disposed,
|
|
102
|
+
busy: pending > 0,
|
|
103
|
+
profiling: options.profiler?.isProfiling() ?? false,
|
|
104
|
+
profilePath: options.profiler?.getProfilePath(),
|
|
105
|
+
...next,
|
|
106
|
+
}
|
|
107
|
+
for (const listener of listeners) {
|
|
108
|
+
try {
|
|
109
|
+
listener()
|
|
110
|
+
} catch {
|
|
111
|
+
/* Observers cannot interrupt recording ownership. */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const reportError = (error: unknown) => {
|
|
116
|
+
publish({ error: String(error) })
|
|
117
|
+
try {
|
|
118
|
+
options.onError?.(error)
|
|
119
|
+
} catch {
|
|
120
|
+
/* Diagnostics cannot change measured operations. */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const safely = <T>(work: () => T): T | undefined => {
|
|
124
|
+
try {
|
|
125
|
+
return work()
|
|
126
|
+
} catch (error) {
|
|
127
|
+
reportError(error)
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const emit = (entry: TracingEntry) => {
|
|
132
|
+
if (options.sink)
|
|
133
|
+
safely(() => options.sink!(formatTracingEntry(entry), entry))
|
|
134
|
+
}
|
|
135
|
+
const enqueue = (work: () => Promise<void>) => {
|
|
136
|
+
if (disposed) return Promise.reject(new Error('Trace client is disposed'))
|
|
137
|
+
pending++
|
|
138
|
+
publish()
|
|
139
|
+
const result = tail.then(async () => {
|
|
140
|
+
try {
|
|
141
|
+
if (disposed) throw new Error('Trace client is disposed')
|
|
142
|
+
await work()
|
|
143
|
+
} finally {
|
|
144
|
+
pending--
|
|
145
|
+
publish()
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
tail = result.catch(reportError)
|
|
149
|
+
return result
|
|
150
|
+
}
|
|
151
|
+
const stopCurrent = async () => {
|
|
152
|
+
enabled = false
|
|
153
|
+
try {
|
|
154
|
+
await handle?.stop()
|
|
155
|
+
} finally {
|
|
156
|
+
handle = undefined
|
|
157
|
+
recording?.stop()
|
|
158
|
+
marks.clear()
|
|
159
|
+
publish()
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const startCurrent = async () => {
|
|
163
|
+
await stopCurrent()
|
|
164
|
+
if (disposed) throw new Error('Trace client is disposed')
|
|
165
|
+
recording?.dispose()
|
|
166
|
+
recording = undefined
|
|
167
|
+
const { Tracing } = require('../index') as typeof import('../index')
|
|
168
|
+
recording = Tracing.startRecording({
|
|
169
|
+
// ~1-2 hours of spans for UAT sessions; metric samples use at most half.
|
|
170
|
+
maxEvents: 10000,
|
|
171
|
+
maxBytes: 6 * 1024 * 1024,
|
|
172
|
+
maxActiveSpans: 256,
|
|
173
|
+
spanTimeoutMs: 30 * 60 * 1000,
|
|
174
|
+
...options.recordingOptions,
|
|
175
|
+
})
|
|
176
|
+
publish({ error: undefined, playground: undefined })
|
|
177
|
+
try {
|
|
178
|
+
const plugins = [...(options.plugins ?? [])]
|
|
179
|
+
const api =
|
|
180
|
+
options.performance === false
|
|
181
|
+
? undefined
|
|
182
|
+
: (options.performance ?? defaultPerformanceApi())
|
|
183
|
+
if (api)
|
|
184
|
+
plugins.unshift(
|
|
185
|
+
createPerformancePlugin(api, options.sink ? emit : undefined)
|
|
186
|
+
)
|
|
187
|
+
if (options.runtimeMetrics)
|
|
188
|
+
plugins.push(
|
|
189
|
+
createRuntimeMetricsPlugin(
|
|
190
|
+
options.runtimeMetrics === true ? {} : options.runtimeMetrics
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
if (options.profiler) plugins.push(options.profiler)
|
|
194
|
+
handle = startPlugins(recording, plugins, reportError)
|
|
195
|
+
publish({ collectors: plugins.map((plugin) => plugin.id) })
|
|
196
|
+
} catch (error) {
|
|
197
|
+
try {
|
|
198
|
+
await stopCurrent()
|
|
199
|
+
} catch (cleanupError) {
|
|
200
|
+
reportError(cleanupError)
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
recording?.dispose()
|
|
204
|
+
} finally {
|
|
205
|
+
recording = undefined
|
|
206
|
+
}
|
|
207
|
+
throw error
|
|
208
|
+
}
|
|
209
|
+
enabled = true
|
|
210
|
+
if (options.autoProfile && options.profiler)
|
|
211
|
+
safely(() => options.profiler!.startProfiling())
|
|
212
|
+
publish()
|
|
213
|
+
}
|
|
214
|
+
const context = (name: string, attributes?: ClientAttributes) => ({
|
|
215
|
+
name,
|
|
216
|
+
correlationId: String(attributes?.id ?? ''),
|
|
217
|
+
attributes: Object.entries(attributes ?? {})
|
|
218
|
+
.filter(([, value]) => value !== undefined)
|
|
219
|
+
.slice(0, 32)
|
|
220
|
+
.map(([key, value]) => ({
|
|
221
|
+
key: key.slice(0, 64),
|
|
222
|
+
value: String(value).slice(0, 512),
|
|
223
|
+
})),
|
|
224
|
+
})
|
|
225
|
+
const trace = {
|
|
226
|
+
mark(name: string, attributes?: ClientAttributes) {
|
|
227
|
+
safely(() => {
|
|
228
|
+
if (!enabled || !recording) return
|
|
229
|
+
recording.mark(context(name, attributes))
|
|
230
|
+
emit({ name, entryType: 'mark', durationMs: 0, attributes })
|
|
231
|
+
if (typeof attributes?.id === 'string') {
|
|
232
|
+
let bucket = marks.get(attributes.id)
|
|
233
|
+
if (!bucket) {
|
|
234
|
+
if (marks.size >= 512) marks.delete(marks.keys().next().value!)
|
|
235
|
+
bucket = { marks: new Map(), emitted: new Set() }
|
|
236
|
+
marks.set(attributes.id, bucket)
|
|
237
|
+
}
|
|
238
|
+
if (bucket.marks.size >= 64 && !bucket.marks.has(name))
|
|
239
|
+
bucket.marks.delete(bucket.marks.keys().next().value!)
|
|
240
|
+
bucket.marks.set(name, recording.getStats().nowMs)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
},
|
|
244
|
+
metric(
|
|
245
|
+
name: string,
|
|
246
|
+
value: number | string,
|
|
247
|
+
attributes?: ClientAttributes
|
|
248
|
+
) {
|
|
249
|
+
safely(() => {
|
|
250
|
+
if (!enabled || !recording) return
|
|
251
|
+
const numeric = Number(value)
|
|
252
|
+
if (Number.isFinite(numeric))
|
|
253
|
+
recording.recordMetric({
|
|
254
|
+
...context(name, attributes),
|
|
255
|
+
value: numeric,
|
|
256
|
+
unit: String(attributes?.unit ?? 'count'),
|
|
257
|
+
})
|
|
258
|
+
else recording.mark(context(name, { ...attributes, value }))
|
|
259
|
+
emit({ name, entryType: 'metric', durationMs: 0, value, attributes })
|
|
260
|
+
})
|
|
261
|
+
},
|
|
262
|
+
async measure<T>(
|
|
263
|
+
name: string,
|
|
264
|
+
fn: () => Promise<T>,
|
|
265
|
+
attributes?: ClientAttributes
|
|
266
|
+
): Promise<T> {
|
|
267
|
+
const started =
|
|
268
|
+
enabled && options.sink ? globalThis.performance.now() : undefined
|
|
269
|
+
const span = safely(() =>
|
|
270
|
+
enabled ? recording?.startSpan(context(name, attributes)) : undefined
|
|
271
|
+
)
|
|
272
|
+
try {
|
|
273
|
+
const value = await fn()
|
|
274
|
+
safely(() => span?.end('success'))
|
|
275
|
+
return value
|
|
276
|
+
} catch (error) {
|
|
277
|
+
safely(() => span?.end('error'))
|
|
278
|
+
throw error
|
|
279
|
+
} finally {
|
|
280
|
+
if (started !== undefined)
|
|
281
|
+
emit({
|
|
282
|
+
name,
|
|
283
|
+
entryType: 'measure',
|
|
284
|
+
durationMs: Math.max(0, globalThis.performance.now() - started),
|
|
285
|
+
attributes,
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
measureSince(
|
|
290
|
+
name: string,
|
|
291
|
+
startName: string,
|
|
292
|
+
id: string,
|
|
293
|
+
attributes?: ClientAttributes
|
|
294
|
+
) {
|
|
295
|
+
safely(() => {
|
|
296
|
+
if (!enabled || !recording) return
|
|
297
|
+
const bucket = marks.get(id)
|
|
298
|
+
const start = bucket?.marks.get(startName)
|
|
299
|
+
if (
|
|
300
|
+
start === undefined ||
|
|
301
|
+
bucket!.emitted.has(name) ||
|
|
302
|
+
bucket!.emitted.size >= 64
|
|
303
|
+
)
|
|
304
|
+
return
|
|
305
|
+
bucket!.emitted.add(name)
|
|
306
|
+
const durationMs = Math.max(0, recording.getStats().nowMs - start)
|
|
307
|
+
recording.recordSpan({
|
|
308
|
+
...context(name, { ...attributes, id, startName }),
|
|
309
|
+
timestampMs: start,
|
|
310
|
+
durationMs,
|
|
311
|
+
outcome: 'success',
|
|
312
|
+
})
|
|
313
|
+
emit({
|
|
314
|
+
name,
|
|
315
|
+
entryType: 'measure',
|
|
316
|
+
durationMs,
|
|
317
|
+
attributes: { ...attributes, id },
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
},
|
|
321
|
+
clear(id: string) {
|
|
322
|
+
marks.delete(id)
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
/** Once per client lifetime: elapsed time from client creation to the app-defined ready signal. */
|
|
327
|
+
reportAppReady() {
|
|
328
|
+
if (readyReported || !enabled) return
|
|
329
|
+
readyReported = true
|
|
330
|
+
trace.metric(
|
|
331
|
+
'app.ready.after_tracer_init',
|
|
332
|
+
Math.max(0, globalThis.performance.now() - readinessStartMs),
|
|
333
|
+
{
|
|
334
|
+
unit: 'ms',
|
|
335
|
+
source: 'app-readiness',
|
|
336
|
+
definition:
|
|
337
|
+
'Tracer initialization to app ready signal; excludes earlier JS and native startup',
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
},
|
|
341
|
+
trace,
|
|
342
|
+
start: () => enqueue(startCurrent),
|
|
343
|
+
stop: () => enqueue(stopCurrent),
|
|
344
|
+
clear: () =>
|
|
345
|
+
enqueue(async () => {
|
|
346
|
+
const wasEnabled = enabled
|
|
347
|
+
await startCurrent()
|
|
348
|
+
if (!wasEnabled) await stopCurrent()
|
|
349
|
+
}),
|
|
350
|
+
getRecording: () => recording,
|
|
351
|
+
getError: () => snapshot.error,
|
|
352
|
+
getSnapshot: () => snapshot,
|
|
353
|
+
subscribe(listener: () => void) {
|
|
354
|
+
listeners.add(listener)
|
|
355
|
+
return () => {
|
|
356
|
+
listeners.delete(listener)
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
open() {
|
|
360
|
+
if (!disposed) publish({ visible: true })
|
|
361
|
+
},
|
|
362
|
+
close() {
|
|
363
|
+
publish({ visible: false })
|
|
364
|
+
},
|
|
365
|
+
reportError,
|
|
366
|
+
canShare: Boolean(options.share),
|
|
367
|
+
canProfile: Boolean(options.profiler),
|
|
368
|
+
canShareProfile: Boolean(options.shareProfile),
|
|
369
|
+
export: () =>
|
|
370
|
+
enqueue(async () => {
|
|
371
|
+
if (!recording || !options.share) return
|
|
372
|
+
await stopCurrent()
|
|
373
|
+
await options.share(recording, 'recording')
|
|
374
|
+
}),
|
|
375
|
+
/** Share a snapshot while recording continues, e.g. right after catching a problem. */
|
|
376
|
+
shareTrace: (format: TraceFormat = 'perfetto') =>
|
|
377
|
+
enqueue(async () => {
|
|
378
|
+
if (recording && options.share) await options.share(recording, format)
|
|
379
|
+
}),
|
|
380
|
+
/**
|
|
381
|
+
* Mark the moment a tester saw a problem. With an idle profiler this also captures
|
|
382
|
+
* a bounded Hermes profile, so the flag ships with what the JS thread was doing.
|
|
383
|
+
*/
|
|
384
|
+
flag: (note?: string) => {
|
|
385
|
+
trace.mark('flag', { source: 'flag', note })
|
|
386
|
+
const profiler = options.profiler
|
|
387
|
+
const windowMs = options.flagProfileMs ?? 10000
|
|
388
|
+
if (!profiler || windowMs <= 0) return
|
|
389
|
+
// enqueue already reports failures; the catches only avoid unhandled rejections.
|
|
390
|
+
enqueue(async () => {
|
|
391
|
+
if (!enabled || profiler.isProfiling()) return
|
|
392
|
+
profiler.startProfiling()
|
|
393
|
+
setTimeout(() => {
|
|
394
|
+
enqueue(async () => {
|
|
395
|
+
if (profiler.isProfiling()) await profiler.stopProfiling()
|
|
396
|
+
}).catch(() => {})
|
|
397
|
+
}, windowMs)
|
|
398
|
+
}).catch(() => {})
|
|
399
|
+
},
|
|
400
|
+
toggleProfile: () =>
|
|
401
|
+
enqueue(async () => {
|
|
402
|
+
if (!enabled || !options.profiler) return
|
|
403
|
+
if (options.profiler.isProfiling())
|
|
404
|
+
await options.profiler.stopProfiling()
|
|
405
|
+
else options.profiler.startProfiling()
|
|
406
|
+
}),
|
|
407
|
+
shareProfile: () =>
|
|
408
|
+
enqueue(async () => {
|
|
409
|
+
const path = options.profiler?.getProfilePath()
|
|
410
|
+
if (path) await options.shareProfile?.(path)
|
|
411
|
+
}),
|
|
412
|
+
playground: () =>
|
|
413
|
+
enqueue(async () => {
|
|
414
|
+
if (enabled && recording)
|
|
415
|
+
publish({ playground: await recordPlayground(recording) })
|
|
416
|
+
}),
|
|
417
|
+
dispose() {
|
|
418
|
+
if (disposed) return tail
|
|
419
|
+
disposed = true
|
|
420
|
+
enabled = false
|
|
421
|
+
publish({ visible: false })
|
|
422
|
+
const result = tail.then(async () => {
|
|
423
|
+
try {
|
|
424
|
+
await stopCurrent()
|
|
425
|
+
} finally {
|
|
426
|
+
try {
|
|
427
|
+
recording?.dispose()
|
|
428
|
+
} finally {
|
|
429
|
+
recording = undefined
|
|
430
|
+
listeners.clear()
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
tail = result.catch(reportError)
|
|
435
|
+
return result
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
export type TraceClient = ReturnType<typeof createTraceClient>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Recording } from '../specs/Recording.nitro'
|
|
2
|
+
import type { SpanEvent, MetricEvent } from '../types'
|
|
3
|
+
export type PlaygroundCheckId =
|
|
4
|
+
'nested' | 'concurrency' | 'cancelled' | 'idempotent' | 'error' | 'metrics'
|
|
5
|
+
export interface PlaygroundReport {
|
|
6
|
+
correlationId: string
|
|
7
|
+
checks: { id: PlaygroundCheckId; passed: boolean }[]
|
|
8
|
+
passed: boolean
|
|
9
|
+
}
|
|
10
|
+
const delay = (ms: number) =>
|
|
11
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
12
|
+
/** Adds a small isolated scenario; never resets or stops the user's recording. */
|
|
13
|
+
export const recordPlayground = async (
|
|
14
|
+
current: Recording
|
|
15
|
+
): Promise<PlaygroundReport> => {
|
|
16
|
+
const identity = current.startSpan({
|
|
17
|
+
name: 'playground.run',
|
|
18
|
+
correlationId: '',
|
|
19
|
+
attributes: [],
|
|
20
|
+
})
|
|
21
|
+
const correlationId = `playground-${identity.spanId}`
|
|
22
|
+
identity.end('success')
|
|
23
|
+
const root = current.startSpan({
|
|
24
|
+
name: 'playground.pipeline',
|
|
25
|
+
correlationId,
|
|
26
|
+
attributes: [{ key: 'source', value: 'playground' }],
|
|
27
|
+
})
|
|
28
|
+
const start = (name: string) =>
|
|
29
|
+
current.startSpan({
|
|
30
|
+
name,
|
|
31
|
+
correlationId,
|
|
32
|
+
parentSpanId: root.spanId,
|
|
33
|
+
attributes: [],
|
|
34
|
+
})
|
|
35
|
+
try {
|
|
36
|
+
const first = start('playground.concurrent')
|
|
37
|
+
const second = start('playground.concurrent')
|
|
38
|
+
await Promise.all([
|
|
39
|
+
delay(30).then(() => first.end('success')),
|
|
40
|
+
delay(60).then(() => second.end('success')),
|
|
41
|
+
])
|
|
42
|
+
const cancelled = start('playground.cancelled')
|
|
43
|
+
cancelled.end('cancelled')
|
|
44
|
+
const repeated = start('playground.idempotent')
|
|
45
|
+
repeated.end('success')
|
|
46
|
+
repeated.end('error')
|
|
47
|
+
const failed = start('playground.error')
|
|
48
|
+
failed.end('error')
|
|
49
|
+
for (const value of [20, 45, 80])
|
|
50
|
+
current.recordMetric({
|
|
51
|
+
name: 'playground.throughput',
|
|
52
|
+
correlationId,
|
|
53
|
+
attributes: [],
|
|
54
|
+
value,
|
|
55
|
+
unit: 'byte/second',
|
|
56
|
+
})
|
|
57
|
+
root.end('success')
|
|
58
|
+
const spans: SpanEvent[] = []
|
|
59
|
+
const metrics: MetricEvent[] = []
|
|
60
|
+
let afterSequence = 0
|
|
61
|
+
// Native recordings cap at 100000 events. Retain only this scenario while scanning.
|
|
62
|
+
for (let pageIndex = 0; pageIndex < 100; pageIndex++) {
|
|
63
|
+
const page = await current.readEvents({ afterSequence, limit: 1000 })
|
|
64
|
+
spans.push(
|
|
65
|
+
...page.spans.filter(
|
|
66
|
+
(event) =>
|
|
67
|
+
event.correlationId === correlationId ||
|
|
68
|
+
event.spanId === root.spanId
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
metrics.push(
|
|
72
|
+
...page.metrics.filter((event) => event.correlationId === correlationId)
|
|
73
|
+
)
|
|
74
|
+
if (
|
|
75
|
+
page.nextSequence <= afterSequence ||
|
|
76
|
+
page.spans.length + page.marks.length + page.metrics.length < 1000
|
|
77
|
+
)
|
|
78
|
+
break
|
|
79
|
+
afterSequence = page.nextSequence
|
|
80
|
+
}
|
|
81
|
+
const concurrent = spans.filter(
|
|
82
|
+
(span) => span.name === 'playground.concurrent'
|
|
83
|
+
)
|
|
84
|
+
const repeatedEvents = spans.filter(
|
|
85
|
+
(span) => span.spanId === repeated.spanId
|
|
86
|
+
)
|
|
87
|
+
const checks: PlaygroundReport['checks'] = [
|
|
88
|
+
{
|
|
89
|
+
id: 'nested',
|
|
90
|
+
passed:
|
|
91
|
+
spans.length === 6 &&
|
|
92
|
+
spans
|
|
93
|
+
.filter((span) => span.spanId !== root.spanId)
|
|
94
|
+
.every((span) => span.parentSpanId === root.spanId),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'concurrency',
|
|
98
|
+
passed:
|
|
99
|
+
concurrent.length === 2 &&
|
|
100
|
+
concurrent[0].spanId !== concurrent[1].spanId &&
|
|
101
|
+
Math.max(...concurrent.map((span) => span.timestampMs)) <=
|
|
102
|
+
Math.min(
|
|
103
|
+
...concurrent.map((span) => span.timestampMs + span.durationMs)
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'cancelled',
|
|
108
|
+
passed: spans.some(
|
|
109
|
+
(span) =>
|
|
110
|
+
span.spanId === cancelled.spanId && span.outcome === 'cancelled'
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'idempotent',
|
|
115
|
+
passed:
|
|
116
|
+
repeatedEvents.length === 1 &&
|
|
117
|
+
repeatedEvents[0].outcome === 'success',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'error',
|
|
121
|
+
passed: spans.some(
|
|
122
|
+
(span) => span.spanId === failed.spanId && span.outcome === 'error'
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'metrics',
|
|
127
|
+
passed:
|
|
128
|
+
metrics.length === 3 &&
|
|
129
|
+
metrics.map((metric) => metric.value).join(',') === '20,45,80',
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
return {
|
|
133
|
+
correlationId,
|
|
134
|
+
checks,
|
|
135
|
+
passed: checks.every((check) => check.passed),
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
try {
|
|
139
|
+
root.end('error')
|
|
140
|
+
} catch {}
|
|
141
|
+
throw error
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as FileSystem from 'expo-file-system/legacy'
|
|
2
|
+
import * as Sharing from 'expo-sharing'
|
|
3
|
+
import type { TraceClient, TraceFormat } from '../client/createTraceClient'
|
|
4
|
+
import type { Recording } from '../specs/Recording.nitro'
|
|
5
|
+
export interface ExpoSharingOptions {
|
|
6
|
+
dialogTitle?: string
|
|
7
|
+
}
|
|
8
|
+
/** Native OS file sharing. No remote upload or file-path export to telemetry. */
|
|
9
|
+
export function createExpoTraceSharing(options: ExpoSharingOptions = {}) {
|
|
10
|
+
const shareFile = async (uri: string, mimeType: string) => {
|
|
11
|
+
if (!(await Sharing.isAvailableAsync()))
|
|
12
|
+
throw new Error('Native file sharing is unavailable on this device')
|
|
13
|
+
await Sharing.shareAsync(uri, {
|
|
14
|
+
mimeType,
|
|
15
|
+
dialogTitle: options.dialogTitle ?? 'Share performance recording',
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
async share(recording: Recording, format: TraceFormat = 'recording') {
|
|
20
|
+
if (!FileSystem.cacheDirectory)
|
|
21
|
+
throw new Error('Application cache is unavailable')
|
|
22
|
+
const prefix = format === 'perfetto' ? 'perfetto-trace' : 'native-trace'
|
|
23
|
+
const uri = `${FileSystem.cacheDirectory}${prefix}-${
|
|
24
|
+
recording.getStats().sessionId
|
|
25
|
+
}-${Date.now()}.json`
|
|
26
|
+
// Both formats serialize off the JS thread; only the finished string crosses JSI.
|
|
27
|
+
const json =
|
|
28
|
+
format === 'perfetto'
|
|
29
|
+
? await recording.exportTraceEvents()
|
|
30
|
+
: await recording.exportJson()
|
|
31
|
+
try {
|
|
32
|
+
await FileSystem.writeAsStringAsync(uri, json, {
|
|
33
|
+
encoding: FileSystem.EncodingType.UTF8,
|
|
34
|
+
})
|
|
35
|
+
await shareFile(uri, 'application/json')
|
|
36
|
+
} finally {
|
|
37
|
+
await FileSystem.deleteAsync(uri, { idempotent: true })
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async shareProfile(path: string) {
|
|
41
|
+
await shareFile(
|
|
42
|
+
path.startsWith('file://') ? path : `file://${path}`,
|
|
43
|
+
'application/octet-stream'
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Register only in a development build. The consumer owns client disposal. */
|
|
49
|
+
export async function registerTraceDevMenu(
|
|
50
|
+
client: TraceClient,
|
|
51
|
+
options: {
|
|
52
|
+
name?: string
|
|
53
|
+
registerDevMenuItems?: typeof import('expo-dev-client').registerDevMenuItems
|
|
54
|
+
} = {}
|
|
55
|
+
) {
|
|
56
|
+
try {
|
|
57
|
+
const register =
|
|
58
|
+
options.registerDevMenuItems ??
|
|
59
|
+
(require('expo-dev-client') as typeof import('expo-dev-client'))
|
|
60
|
+
.registerDevMenuItems
|
|
61
|
+
await register([
|
|
62
|
+
{
|
|
63
|
+
name: options.name ?? 'Performance inspector',
|
|
64
|
+
shouldCollapse: true,
|
|
65
|
+
callback: () => client.open(),
|
|
66
|
+
},
|
|
67
|
+
])
|
|
68
|
+
} catch (error) {
|
|
69
|
+
client.reportError(error)
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NitroModules } from 'react-native-nitro-modules'
|
|
2
|
+
import type { Tracing as TracingFactory } from './specs/Tracing.nitro'
|
|
3
|
+
/** Native recording factory. See Recording for ownership and cleanup. */
|
|
4
|
+
export const Tracing =
|
|
5
|
+
NitroModules.createHybridObject<TracingFactory>('Tracing')
|
|
6
|
+
export type { Recording } from './specs/Recording.nitro'
|
|
7
|
+
export type { TraceSpan } from './specs/TraceSpan.nitro'
|
|
8
|
+
export type * from './types'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Recording } from '../specs/Recording.nitro'
|
|
2
|
+
/** Shared native destination supplied to measurement and export plugins. */
|
|
3
|
+
export interface PluginContext {
|
|
4
|
+
/** Active native recording. */
|
|
5
|
+
recording: Recording
|
|
6
|
+
/** Surface plugin failures without changing application operations. */
|
|
7
|
+
reportError(error: unknown): void
|
|
8
|
+
}
|
|
9
|
+
/** Cleanup owned by a started plugin. */
|
|
10
|
+
export interface PluginHandle {
|
|
11
|
+
/** Disconnect observers and finish export before native recording disposal. */
|
|
12
|
+
stop(): void | Promise<void>
|
|
13
|
+
/** Optional exporter flush after sources stop and native history freezes. */
|
|
14
|
+
flush?(): void | Promise<void>
|
|
15
|
+
}
|
|
16
|
+
/** Optional measurement source, exporter or profiler integration. */
|
|
17
|
+
export interface TracePlugin {
|
|
18
|
+
/** Unique stable configuration identity. */
|
|
19
|
+
readonly id: string
|
|
20
|
+
/** Start once per recording; must roll back resources if setup throws. */
|
|
21
|
+
start(context: PluginContext): PluginHandle
|
|
22
|
+
}
|