Skip to content

Commit a6dee82

Browse files
authored
ref: Use Map() for trace.spans for performance (#690)
1 parent 289a040 commit a6dee82

File tree

11 files changed

+106
-43
lines changed

11 files changed

+106
-43
lines changed

.changeset/sharp-paws-press.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@spotlightjs/spotlight': patch
3+
'@spotlightjs/electron': patch
4+
'@spotlightjs/overlay': patch
5+
'@spotlightjs/astro': patch
6+
---
7+
8+
Make things snappy by using Map() for trace span look ups

packages/overlay/src/integrations/sentry/components/explore/traces/TraceDetails/components/TraceTreeview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export default function TraceTreeview({ traceId }: TraceTreeViewProps) {
3030
<span>&mdash;</span>
3131
<span>
3232
<strong className="text-primary-200 font-bold">{getFormattedSpanDuration(trace)}</strong> recorded in{' '}
33-
<strong className="text-primary-200 font-bold">{trace.spans.length.toLocaleString()} spans</strong>
33+
<strong className="text-primary-200 font-bold">{trace.spans.size.toLocaleString()} spans</strong>
3434
</span>
3535
</div>
3636
</div>

packages/overlay/src/integrations/sentry/components/explore/traces/TraceList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default function TraceList() {
6565
<div>{getFormattedSpanDuration(trace)}</div>
6666
<div>&mdash;</div>
6767
<div>
68-
{trace.spans.length.toLocaleString()} spans, {trace.transactions.length.toLocaleString()} txns
68+
{trace.spans.size.toLocaleString()} spans, {trace.transactions.length.toLocaleString()} txns
6969
</div>
7070
</div>
7171
</div>

packages/overlay/src/integrations/sentry/components/insights/Resources.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const calculateResourceInfo = ({ resource, spanData }: { resource: string; spanD
2727
const avgDuration = totalTimeInMs / specificResources.length;
2828
const avgEncodedSize =
2929
specificResources.reduce((acc: number, span: Span) => {
30-
const contentLength = span.data && span.data['http.response_content_length'];
30+
const contentLength = span.data?.['http.response_content_length'];
3131
if (typeof contentLength === 'number') {
3232
return acc + contentLength;
3333
}

packages/overlay/src/integrations/sentry/components/insights/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useState } from 'react';
22
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
33
import Tabs from '~/components/Tabs';
44
import { useSpotlightContext } from '~/lib/useSpotlightContext';
5-
import { useSentrySpans } from '../../data/useSentrySpans';
5+
import { useSentrySpanCounts } from '../../data/useSentrySpans';
66
import { createTab } from '../../utils/tabs';
77
import HiddenItemsButton from '../HiddenItemsButton';
88
import Queries from './Queries';
@@ -13,11 +13,11 @@ import WebVitalsDetail from './webVitals/WebVitalsDetail';
1313

1414
export default function InsightsTabDetails() {
1515
const context = useSpotlightContext();
16-
const { allSpans, localSpans } = useSentrySpans();
16+
const { allSpans, localSpans } = useSentrySpanCounts();
1717

1818
const [showAll, setShowAll] = useState(!context.experiments['sentry:focus-local-events']);
1919

20-
const hiddenItemCount = allSpans.length - localSpans.length;
20+
const hiddenItemCount = allSpans - localSpans;
2121

2222
const tabs = [
2323
createTab('queries', 'Queries'),

packages/overlay/src/integrations/sentry/data/sentryDataCache.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ class SentryDataCache {
4141
event_id?: string;
4242
})[] = [],
4343
) {
44-
initial.forEach(e => this.pushEvent(e));
44+
for (const evt of initial) {
45+
this.pushEvent(evt);
46+
}
4547
}
4648

4749
setSidecarUrl(url: string) {
@@ -153,9 +155,9 @@ class SentryDataCache {
153155

154156
// recompute tree as we might have txn out of order
155157
// XXX: we're trusting timestamps, which are not trustworthy
156-
const allSpans: Span[] = [];
157-
trace.transactions.forEach(txn => {
158-
allSpans.push({
158+
const spanMap: Map<string, Span> = new Map();
159+
for (const txn of trace.transactions) {
160+
spanMap.set(txn.contexts.trace.span_id, {
159161
...txn.contexts.trace,
160162
tags: txn?.tags,
161163
start_timestamp: txn.start_timestamp,
@@ -165,16 +167,16 @@ class SentryDataCache {
165167
});
166168

167169
if (txn.spans) {
168-
allSpans.push(
169-
...txn.spans.map(s => ({
170-
...s,
171-
timestamp: toTimestamp(s.timestamp),
172-
start_timestamp: toTimestamp(s.start_timestamp),
173-
})),
174-
);
170+
for (const span of txn.spans) {
171+
spanMap.set(span.span_id, {
172+
...span,
173+
timestamp: toTimestamp(span.timestamp),
174+
start_timestamp: toTimestamp(span.start_timestamp),
175+
});
176+
}
175177
}
176-
});
177-
trace.spans = allSpans;
178+
}
179+
trace.spans = spanMap;
178180
trace.spanTree = groupSpans(trace.spans);
179181
} else {
180182
trace.errors += 1;
@@ -232,7 +234,7 @@ class SentryDataCache {
232234
getSpanById(traceId: string, spanId: string) {
233235
const trace = this.tracesById[traceId];
234236
if (!trace) return undefined;
235-
return trace.spans.find(s => s.span_id === spanId);
237+
return trace.spans.get(spanId);
236238
}
237239

238240
resetData() {
Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,41 @@
11
import { useContext } from 'react';
2-
import { Span, Trace } from '../types';
2+
import type { Span, Trace } from '../types';
33
import sentryDataCache from './sentryDataCache';
44
import { SentryEventsContext } from './sentryEventsContext';
55
import { useSentryHelpers } from './useSentryHelpers';
66

7-
export const useSentrySpans = () => {
7+
export function useSentryTraces() {
88
useContext(SentryEventsContext);
99
const helpers = useSentryHelpers();
1010
const allTraces = sentryDataCache.getTraces();
1111
const localTraces = allTraces.filter(t => helpers.isLocalToSession(t.trace_id) !== false);
1212

13-
const allSpans: Span[] = allTraces.reduce((acc: Span[], trace: Trace) => [...acc, ...trace.spans], []);
14-
const localSpans: Span[] = localTraces.reduce((acc: Span[], trace: Trace) => [...acc, ...trace.spans], []);
13+
return { allTraces, localTraces };
14+
}
15+
16+
function spanReducer(acc: Span[], trace: Trace) {
17+
for (const span of trace.spans.values()) {
18+
acc.push(span);
19+
}
20+
return acc;
21+
}
22+
23+
function spanCountReducer(sum: number, trace: Trace) {
24+
return sum + trace.spans.size;
25+
}
26+
27+
export const useSentrySpans = () => {
28+
const { allTraces, localTraces } = useSentryTraces();
29+
const allSpans: Span[] = allTraces.reduce(spanReducer, []);
30+
const localSpans: Span[] = localTraces.reduce(spanReducer, []);
1531
return { allSpans, localSpans };
1632
};
33+
34+
export const useSentrySpanCounts = () => {
35+
const { allTraces, localTraces } = useSentryTraces();
36+
37+
return {
38+
allSpans: allTraces.reduce(spanCountReducer, 0),
39+
localSpans: localTraces.reduce(spanCountReducer, 0),
40+
};
41+
};

packages/overlay/src/integrations/sentry/sentry-integration.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,13 @@ export const spotlightIntegration = () => {
4848
* Flags if the event is a transaction created from an interaction with the spotlight UI.
4949
*/
5050
function isSpotlightInteraction(event: Event): boolean {
51-
return (
52-
(event.type === 'transaction' &&
53-
event.contexts?.trace?.op === 'ui.action.click' &&
54-
event.spans?.some(s => s.description?.includes('#sentry-spotlight'))) ||
55-
false
56-
);
51+
if (event.type === 'transaction' && event.contexts?.trace?.op === 'ui.action.click' && event.spans) {
52+
for (const span of event.spans.values()) {
53+
if (span.description?.includes('#sentry-spotlight')) {
54+
return true;
55+
}
56+
}
57+
}
58+
59+
return false;
5760
}

packages/overlay/src/integrations/sentry/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export type Trace = TraceContext & {
135135
status: string;
136136
rootTransaction: SentryTransactionEvent | null;
137137
rootTransactionName: string;
138-
spans: Span[];
138+
spans: Map<string, Span>;
139139
spanTree: Span[];
140140
};
141141

packages/overlay/src/integrations/sentry/utils/traces.spec.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function mockSpan({ duration, ...span }: Partial<Span> & { duration?: number } =
1818

1919
describe('groupSpans', () => {
2020
test('empty span list', () => {
21-
expect(groupSpans([])).toEqual([]);
21+
expect(groupSpans(new Map())).toEqual([]);
2222
});
2323

2424
test('simple parent and child relationship', () => {
@@ -31,7 +31,13 @@ describe('groupSpans', () => {
3131
parent_span_id: parent1.span_id,
3232
trace_id: parent1.trace_id,
3333
});
34-
const result = groupSpans([parent1, span1, span2]);
34+
const result = groupSpans(
35+
new Map<string, Span>([
36+
[parent1.span_id, parent1],
37+
[span1.span_id, span1],
38+
[span2.span_id, span2],
39+
]),
40+
);
3541
console.debug(result);
3642
expect(result.length).toEqual(1);
3743
expect(result[0].span_id).toEqual(parent1.span_id);
@@ -63,7 +69,16 @@ describe('groupSpans', () => {
6369
parent_span_id: parent2.span_id,
6470
trace_id: parent2.trace_id,
6571
});
66-
const result = groupSpans([parent1, span1, span2, parent2, span3, span4]);
72+
const result = groupSpans(
73+
new Map<string, Span>([
74+
[parent1.span_id, parent1],
75+
[span1.span_id, span1],
76+
[span2.span_id, span2],
77+
[parent2.span_id, parent2],
78+
[span3.span_id, span3],
79+
[span4.span_id, span4],
80+
]),
81+
);
6782
console.debug(result);
6883
expect(result.length).toEqual(2);
6984
expect(result[0].span_id).toEqual(parent1.span_id);
@@ -85,7 +100,12 @@ describe('groupSpans', () => {
85100
parent_span_id,
86101
trace_id: span1.trace_id,
87102
});
88-
const result = groupSpans([span1, span2]);
103+
const result = groupSpans(
104+
new Map<string, Span>([
105+
[span1.span_id, span1],
106+
[span2.span_id, span2],
107+
]),
108+
);
89109
console.debug(result);
90110
expect(result.length).toEqual(1);
91111
expect(result[0].op).toEqual('orphan');
@@ -106,7 +126,12 @@ describe('groupSpans', () => {
106126
parent_span_id: generateUuidv4(),
107127
trace_id: span1.trace_id,
108128
});
109-
const result = groupSpans([span1, span2]);
129+
const result = groupSpans(
130+
new Map<string, Span>([
131+
[span1.span_id, span1],
132+
[span2.span_id, span2],
133+
]),
134+
);
110135
console.debug(result);
111136
expect(result.length).toEqual(2);
112137
expect(result[0].span_id).toEqual(span1.parent_span_id);

0 commit comments

Comments
 (0)