-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathperformance.js
More file actions
227 lines (206 loc) · 7.92 KB
/
performance.js
File metadata and controls
227 lines (206 loc) · 7.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import logger from './logger-instance'
import {createChildSpan, endSpan, logPerformanceMetric} from './opentelemetry'
export const PERFORMANCE_MARKS = {
total: 'ssr.total',
renderToString: 'ssr.render-to-string',
routeMatching: 'ssr.route-matching',
loadComponent: 'ssr.load-component',
fetchStrategies: 'ssr.fetch-strategies',
reactQueryPrerender: 'ssr.fetch-strategies.react-query.pre-render',
reactQueryUseQuery: 'ssr.fetch-strategies.react-query.use-query',
getProps: 'ssr.fetch-strategies.get-prop'
}
/**
* This is an SDK internal class that is responsible for measuring server side performance.
*
* This class manages two types of performance marks: start and end.
*
* By default, this timer is disabled. Only certain environment variables and feature flags turns it on.
*
* @private
*/
export default class PerformanceTimer {
MARKER_TYPES = {
START: 'start',
END: 'end'
}
constructor(options = {}) {
this.enabled = options.enabled || false
this.metrics = []
this.spans = new Map()
this.spanTimeouts = new Map()
this.maxSpanDuration = options.maxSpanDuration || 30000 // 30 seconds default
}
/**
* This is a utility function to build the Server-Timing header.
* The function receives an array of performance metrics and returns a string that represents the Server-Timing header.
*
* see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
*
* @function
* @private
*
* @return {String}
*/
buildServerTimingHeader() {
const header = this.metrics
.map((metric) => {
return `${metric.name};dur=${metric.duration.toFixed(2)}`
})
.join(', ')
return header
}
/**
* Logs all performance metrics
*/
log() {
// Log each metric once with the standardized format
this.metrics.forEach((metric) => {
logPerformanceMetric(metric.name, metric.duration, {
'performance.detail': metric.detail || ''
})
})
}
/**
* This is a utility function to create performance marks.
* The data will be used in console logs and the http response header `server-timing`.
*
* @param {string} name - Unique identifier for the performance measurement.
* Must be the same for both start and end marks of a pair. E.g. 'ssr.render-to-string'
*
* @param {string} type - Mark type, either 'start' or 'end'. 'start' creates spans and browser marks,
* 'end' completes measurement and cleanup.
*
* @param {Object} [options={}] - Optional configuration object
* @param {string|Object} [options.detail=''] - Additional metadata for the mark
* included in logs and tracing attributes.
*/
mark(name, type, options = {}) {
const {detail = ''} = options
if (!name || !type || !this.enabled) {
return
}
if (type !== this.MARKER_TYPES.START && type !== this.MARKER_TYPES.END) {
logger.warn('Invalid mark type', {type, name, namespace: 'PerformanceTimer.mark'})
return
}
try {
// Format detail as a string if it's an object
const formattedDetail = typeof detail === 'object' ? JSON.stringify(detail) : detail
performance.mark(`${name}.${type}`, {
detail: formattedDetail
})
// Only create spans for 'start' events and store them for later use
if (type === this.MARKER_TYPES.START) {
if (!this.spans.has(name)) {
const span = createChildSpan(name, {
performance_mark: name,
performance_type: type,
performance_detail: formattedDetail
})
if (span) {
this.spans.set(name, span)
// Set up automatic cleanup for orphaned spans
const timeoutId = setTimeout(() => {
this._cleanupOrphanedSpan(name, 'timeout')
}, this.maxSpanDuration)
this.spanTimeouts.set(name, timeoutId)
}
}
} else if (type === this.MARKER_TYPES.END) {
const startMark = `${name}.${this.MARKER_TYPES.START}`
const endMark = `${name}.${this.MARKER_TYPES.END}`
try {
const measure = performance.measure(name, startMark, endMark)
// Add the metric to the metrics array for Server-Timing header
this.metrics.push({
name,
duration: measure.duration,
detail: formattedDetail
})
// End the corresponding span if it exists and clear timeout
const span = this.spans.get(name)
if (span) {
endSpan(span)
this.spans.delete(name)
// Clear the timeout since span completed normally
const timeoutId = this.spanTimeouts.get(name)
if (timeoutId) {
clearTimeout(timeoutId)
this.spanTimeouts.delete(name)
}
}
// Clear the marks
performance.clearMarks(startMark)
performance.clearMarks(endMark)
performance.clearMeasures(name)
} catch (error) {
logger.warn('Failed to measure performance mark', {
name,
error: error.message,
startMark,
endMark,
namespace: 'PerformanceTimer.mark'
})
}
}
} catch (error) {
if (error.name === 'SyntaxError') {
logger.warn('Invalid performance mark name', {name, error: error.message})
} else {
logger.error('Error creating performance mark', {
name,
type,
error: error.message,
stack: error.stack,
namespace: 'PerformanceTimer.mark'
})
}
}
}
/**
* Helper method to clean up a specific orphaned span
* @private
*/
_cleanupOrphanedSpan(name, reason = 'manual') {
const span = this.spans.get(name)
if (span) {
logger.warn('Cleaning up orphaned span', {
name,
error: 'Deleting orphaned span (reason: ' + reason + ' cleanup)',
namespace: 'PerformanceTimer._cleanupOrphanedSpan'
})
endSpan(span)
this.spans.delete(name)
}
// Clear the timeout
const timeoutId = this.spanTimeouts.get(name)
if (timeoutId) {
clearTimeout(timeoutId)
this.spanTimeouts.delete(name)
}
}
/**
* Clean up all orphaned spans and clear all timeouts
* Call this when the timer is no longer needed or when you want to force cleanup
*/
cleanup() {
// Clean up any orphaned spans
this.spans.forEach((span, name) => {
this._cleanupOrphanedSpan(name, 'manual_cleanup')
})
// Clear any remaining timeouts
this.spanTimeouts.forEach((timeoutId) => {
clearTimeout(timeoutId)
})
this.spanTimeouts.clear()
// Clear metrics as well
this.metrics = []
}
}