@dxos/tracing 0.1.56-main.d9d1c3e
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/LICENSE +8 -0
- package/README.md +3 -0
- package/dist/lib/browser/index.mjs +308 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/node/index.cjs +342 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/types/src/api.d.ts +10 -0
- package/dist/types/src/api.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/symbols.d.ts +7 -0
- package/dist/types/src/symbols.d.ts.map +1 -0
- package/dist/types/src/trace-processor.d.ts +61 -0
- package/dist/types/src/trace-processor.d.ts.map +1 -0
- package/dist/types/src/trace-sender.d.ts +9 -0
- package/dist/types/src/trace-sender.d.ts.map +1 -0
- package/dist/types/src/tracing.test.d.ts +2 -0
- package/dist/types/src/tracing.test.d.ts.map +1 -0
- package/package.json +32 -0
- package/src/api.ts +56 -0
- package/src/index.ts +8 -0
- package/src/symbols.ts +17 -0
- package/src/trace-processor.ts +215 -0
- package/src/trace-sender.ts +78 -0
- package/src/tracing.test.ts +58 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Context } from '@dxos/context';
|
|
6
|
+
import { Error as SerializedError } from '@dxos/protocols/proto/dxos/error';
|
|
7
|
+
import { Resource, Span } from '@dxos/protocols/proto/dxos/tracing';
|
|
8
|
+
import { getPrototypeSpecificInstanceId } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
import type { AddLinkOptions } from './api';
|
|
11
|
+
import { TRACE_SPAN_ATTRIBUTE, getTracingContext } from './symbols';
|
|
12
|
+
import { TraceSender } from './trace-sender';
|
|
13
|
+
|
|
14
|
+
export type TraceResourceConstructorParams = {
|
|
15
|
+
constructor: { new (...args: any[]): {} };
|
|
16
|
+
instance: any;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type TraceSpanParams = {
|
|
20
|
+
instance: any;
|
|
21
|
+
methodName: string;
|
|
22
|
+
parentCtx: Context | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ResourceEntry = {
|
|
26
|
+
data: Resource;
|
|
27
|
+
instance: WeakRef<any>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TraceSubscription = {
|
|
31
|
+
dirtyResources: Set<number>;
|
|
32
|
+
dirtySpans: Set<number>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const MAX_RESOURCE_RECORDS = 500;
|
|
36
|
+
const MAX_SPAN_RECORDS = 1_000;
|
|
37
|
+
|
|
38
|
+
export class TraceProcessor {
|
|
39
|
+
resources = new Map<number, ResourceEntry>();
|
|
40
|
+
resourceInstanceIndex = new WeakMap<any, ResourceEntry>();
|
|
41
|
+
resourceIdList: number[] = [];
|
|
42
|
+
|
|
43
|
+
spans = new Map<number, Span>();
|
|
44
|
+
spanIdList: number[] = [];
|
|
45
|
+
|
|
46
|
+
subscriptions: Set<TraceSubscription> = new Set();
|
|
47
|
+
|
|
48
|
+
traceResourceConstructor(params: TraceResourceConstructorParams) {
|
|
49
|
+
const id = this.resources.size;
|
|
50
|
+
const entry: ResourceEntry = {
|
|
51
|
+
data: {
|
|
52
|
+
id,
|
|
53
|
+
className: params.constructor.name,
|
|
54
|
+
instanceId: getPrototypeSpecificInstanceId(params.instance),
|
|
55
|
+
info: this.getResourceInfo(params.instance),
|
|
56
|
+
links: [],
|
|
57
|
+
},
|
|
58
|
+
instance: new WeakRef(params.instance),
|
|
59
|
+
};
|
|
60
|
+
this.resources.set(id, entry);
|
|
61
|
+
this.resourceInstanceIndex.set(params.instance, entry);
|
|
62
|
+
this.resourceIdList.push(id);
|
|
63
|
+
if (this.resourceIdList.length > MAX_RESOURCE_RECORDS) {
|
|
64
|
+
this._clearResources();
|
|
65
|
+
}
|
|
66
|
+
this._markResourceDirty(id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getResourceInfo(instance: any): Record<string, any> {
|
|
70
|
+
const res: Record<string, any> = {};
|
|
71
|
+
const tracingContext = getTracingContext(Object.getPrototypeOf(instance));
|
|
72
|
+
|
|
73
|
+
for (const [key, _opts] of Object.entries(tracingContext.infoProperties)) {
|
|
74
|
+
try {
|
|
75
|
+
res[key] = typeof instance[key] === 'function' ? instance[key]() : instance[key];
|
|
76
|
+
} catch (err: any) {
|
|
77
|
+
res[key] = err.message;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return res;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
traceSpan(params: TraceSpanParams): TracingSpan {
|
|
85
|
+
const span = new TracingSpan(this, params);
|
|
86
|
+
this._flushSpan(span);
|
|
87
|
+
return span;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
addLink(parent: any, child: any, opts: AddLinkOptions) {}
|
|
91
|
+
|
|
92
|
+
getResourceId(instance: any): number | null {
|
|
93
|
+
const entry = this.resourceInstanceIndex.get(instance);
|
|
94
|
+
return entry ? entry.data.id : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
createTraceSender() {
|
|
98
|
+
return new TraceSender(this);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
_flushSpan(runtimeSpan: TracingSpan) {
|
|
105
|
+
const span = runtimeSpan.serialize();
|
|
106
|
+
this.spans.set(span.id, span);
|
|
107
|
+
this.spanIdList.push(span.id);
|
|
108
|
+
if (this.spanIdList.length > MAX_SPAN_RECORDS) {
|
|
109
|
+
this._clearSpans();
|
|
110
|
+
}
|
|
111
|
+
this._markSpanDirty(span.id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private _markResourceDirty(id: number) {
|
|
115
|
+
for (const subscription of this.subscriptions) {
|
|
116
|
+
subscription.dirtyResources.add(id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private _markSpanDirty(id: number) {
|
|
121
|
+
for (const subscription of this.subscriptions) {
|
|
122
|
+
subscription.dirtySpans.add(id);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private _clearResources() {
|
|
127
|
+
// TODO(dmaretskyi): Use FinalizationRegistry to delete finalized resources first.
|
|
128
|
+
while (this.resourceIdList.length > MAX_RESOURCE_RECORDS) {
|
|
129
|
+
const id = this.resourceIdList.shift()!;
|
|
130
|
+
this.resources.delete(id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private _clearSpans() {
|
|
135
|
+
while (this.spanIdList.length > MAX_SPAN_RECORDS) {
|
|
136
|
+
const id = this.spanIdList.shift()!;
|
|
137
|
+
this.spans.delete(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class TracingSpan {
|
|
143
|
+
static nextId = 0;
|
|
144
|
+
|
|
145
|
+
readonly id: number;
|
|
146
|
+
readonly parentId: number | null = null;
|
|
147
|
+
readonly methodName: string;
|
|
148
|
+
readonly resourceId: number | null = null;
|
|
149
|
+
startTs: number;
|
|
150
|
+
endTs: number | null = null;
|
|
151
|
+
error: SerializedError | null = null;
|
|
152
|
+
|
|
153
|
+
private readonly _ctx: Context | null = null;
|
|
154
|
+
|
|
155
|
+
constructor(private _traceProcessor: TraceProcessor, params: TraceSpanParams) {
|
|
156
|
+
this.id = TracingSpan.nextId++;
|
|
157
|
+
this.methodName = params.methodName;
|
|
158
|
+
this.resourceId = _traceProcessor.getResourceId(params.instance);
|
|
159
|
+
this.startTs = performance.now();
|
|
160
|
+
|
|
161
|
+
if (params.parentCtx) {
|
|
162
|
+
this._ctx = params.parentCtx.derive({
|
|
163
|
+
attributes: {
|
|
164
|
+
[TRACE_SPAN_ATTRIBUTE]: this.id,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const parentId = params.parentCtx.getAttribute(TRACE_SPAN_ATTRIBUTE);
|
|
168
|
+
if (typeof parentId === 'number') {
|
|
169
|
+
this.parentId = parentId;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get ctx(): Context | null {
|
|
175
|
+
return this._ctx;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
markSuccess() {
|
|
179
|
+
this.endTs = performance.now();
|
|
180
|
+
this._traceProcessor._flushSpan(this);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
markError(err: unknown) {
|
|
184
|
+
this.endTs = performance.now();
|
|
185
|
+
this.error = serializeError(err);
|
|
186
|
+
this._traceProcessor._flushSpan(this);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
serialize(): Span {
|
|
190
|
+
return {
|
|
191
|
+
id: this.id,
|
|
192
|
+
resourceId: this.resourceId ?? undefined,
|
|
193
|
+
methodName: this.methodName,
|
|
194
|
+
parentId: this.parentId ?? undefined,
|
|
195
|
+
startTs: this.startTs.toFixed(3),
|
|
196
|
+
endTs: this.endTs?.toFixed(3) ?? undefined,
|
|
197
|
+
error: this.error ?? undefined,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const serializeError = (err: unknown): SerializedError => {
|
|
203
|
+
if (err instanceof Error) {
|
|
204
|
+
return {
|
|
205
|
+
name: err.name,
|
|
206
|
+
message: err.message,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
message: String(err),
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const TRACE_PROCESSOR: TraceProcessor = ((globalThis as any).TRACE_PROCESSOR ??= new TraceProcessor());
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Stream } from '@dxos/codec-protobuf';
|
|
6
|
+
import { StreamTraceEvent, TracingService } from '@dxos/protocols/proto/dxos/tracing';
|
|
7
|
+
|
|
8
|
+
import { TraceProcessor, TraceSubscription } from './trace-processor';
|
|
9
|
+
|
|
10
|
+
const FLUSH_INTERVAL = 1_000;
|
|
11
|
+
|
|
12
|
+
export class TraceSender implements TracingService {
|
|
13
|
+
constructor(private _traceProcessor: TraceProcessor) {}
|
|
14
|
+
|
|
15
|
+
streamTrace(request: void): Stream<StreamTraceEvent> {
|
|
16
|
+
return new Stream(({ ctx, next }) => {
|
|
17
|
+
const subscription: TraceSubscription = {
|
|
18
|
+
dirtyResources: new Set(),
|
|
19
|
+
dirtySpans: new Set(),
|
|
20
|
+
};
|
|
21
|
+
this._traceProcessor.subscriptions.add(subscription);
|
|
22
|
+
ctx.onDispose(() => {
|
|
23
|
+
this._traceProcessor.subscriptions.delete(subscription);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const flushEvents = (resources: Set<number> | null, spans: Set<number> | null) => {
|
|
27
|
+
const event: StreamTraceEvent = {
|
|
28
|
+
resourceAdded: [],
|
|
29
|
+
resourceRemoved: [],
|
|
30
|
+
spanAdded: [],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (resources) {
|
|
34
|
+
for (const id of resources) {
|
|
35
|
+
const entry = this._traceProcessor.resources.get(id);
|
|
36
|
+
if (entry) {
|
|
37
|
+
event.resourceAdded!.push({ resource: entry.data });
|
|
38
|
+
} else {
|
|
39
|
+
event.resourceRemoved!.push({ id });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
for (const entry of this._traceProcessor.resources.values()) {
|
|
44
|
+
event.resourceAdded!.push({ resource: entry.data });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (spans) {
|
|
49
|
+
for (const id of spans) {
|
|
50
|
+
const span = this._traceProcessor.spans.get(id);
|
|
51
|
+
if (span) {
|
|
52
|
+
event.spanAdded!.push({ span });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
for (const span of this._traceProcessor.spans.values()) {
|
|
57
|
+
event.spanAdded!.push({ span });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (event.resourceAdded!.length > 0 || event.resourceRemoved!.length > 0 || event.spanAdded!.length > 0) {
|
|
62
|
+
next(event);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
flushEvents(null, null);
|
|
67
|
+
|
|
68
|
+
const timer = setInterval(() => {
|
|
69
|
+
flushEvents(subscription.dirtyResources, subscription.dirtySpans);
|
|
70
|
+
subscription.dirtyResources.clear();
|
|
71
|
+
subscription.dirtySpans.clear();
|
|
72
|
+
}, FLUSH_INTERVAL);
|
|
73
|
+
ctx.onDispose(() => {
|
|
74
|
+
clearInterval(timer);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Context } from '@dxos/context';
|
|
6
|
+
import { log } from '@dxos/log';
|
|
7
|
+
import { test } from '@dxos/test';
|
|
8
|
+
|
|
9
|
+
import { trace } from './api';
|
|
10
|
+
import { TRACE_PROCESSOR } from './trace-processor';
|
|
11
|
+
|
|
12
|
+
@trace.resource()
|
|
13
|
+
class FeedStore {
|
|
14
|
+
private readonly _storage: any;
|
|
15
|
+
|
|
16
|
+
private readonly _feeds = new Map<string, Feed>();
|
|
17
|
+
|
|
18
|
+
@trace.info()
|
|
19
|
+
get storageType() {
|
|
20
|
+
return this._storage.type;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@trace.span()
|
|
24
|
+
async openFeed(ctx: Context) {
|
|
25
|
+
const feed = new Feed();
|
|
26
|
+
feed.key = Math.random().toString(36).substring(2, 15);
|
|
27
|
+
this._feeds.set(feed.key, feed);
|
|
28
|
+
trace.addLink(this, feed, {});
|
|
29
|
+
|
|
30
|
+
await feed.open(ctx);
|
|
31
|
+
return feed;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@trace.resource()
|
|
36
|
+
class Feed {
|
|
37
|
+
@trace.info()
|
|
38
|
+
key!: string;
|
|
39
|
+
|
|
40
|
+
@trace.span()
|
|
41
|
+
async open(ctx: Context) {}
|
|
42
|
+
|
|
43
|
+
@trace.span()
|
|
44
|
+
async close() {
|
|
45
|
+
throw new Error('Not implemented');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('feed store tracing', async () => {
|
|
50
|
+
const store = new FeedStore();
|
|
51
|
+
await store.openFeed(new Context());
|
|
52
|
+
const feed = await store.openFeed(new Context());
|
|
53
|
+
|
|
54
|
+
await feed.close().catch(() => {});
|
|
55
|
+
|
|
56
|
+
log.info('resources', TRACE_PROCESSOR.resources);
|
|
57
|
+
log.info('spans', TRACE_PROCESSOR.spans);
|
|
58
|
+
});
|