@samitouri / QOS-React / commits / 4a702865dd

[Flight] Encode enclosing line/column numbers and use it to align the fake function (#33136)

Stacked on #33135. This encodes the line/column of the enclosing function as part of the stack traces. When that information is available. I adjusted the fake function code generation so that the beginning of the arrow function aligns with these as much as possible. This ensures that when the browser tries to look up the line/column of the enclosing function, such as for getting the function name, it gets the right one. If we can't get the enclosing line/column, then we encode it at the beginning of the file. This is likely to get a miss in the source map identifiers, which means that the function name gets extracted from the runtime name instead which is better. Another thing where this is used is the in the Performance Track. Ideally that would be fixed by https://issues.chromium.org/u/1/issues/415968771 but the enclosing information is useful for other things like the function name resolution anyway. We can also use this for the "View source for this element" in React DevTools.

Sebastian Markbåge committed May 7, 2025 at 12:34 UTC 4a702865dd0c5849c1b454091560c3ef26121611
4 files changed +110 -15
packages/react-client/src/ReactFlightClient.js
+93 -11
@@ -2226,6 +2226,8 @@ function createFakeFunction<T>(
2226 sourceMap: null | string,
2227 line: number,
2228 col: number,
2229 + enclosingLine: number,
2230 + enclosingCol: number,
2231 environmentName: string,
2232 ): FakeFunction<T> {
2233 // This creates a fake copy of a Server Module. It represents a module that has already
@@ -2243,26 +2245,104 @@ function createFakeFunction<T>(
2245 // This allows us to use the original source map as the source map of this fake file to
2246 // point to the original source.
2247 let code;
2246 - if (line <= 1) {
2247 - const minSize = encodedName.length + 7;
2248 + // Normalize line/col to zero based.
2249 + if (enclosingLine < 1) {
2250 + enclosingLine = 0;
2251 + } else {
2252 + enclosingLine--;
2253 + }
2254 + if (enclosingCol < 1) {
2255 + enclosingCol = 0;
2256 + } else {
2257 + enclosingCol--;
2258 + }
2259 + if (line < 1) {
2260 + line = 0;
2261 + } else {
2262 + line--;
2263 + }
2264 + if (col < 1) {
2265 + col = 0;
2266 + } else {
2267 + col--;
2268 + }
2269 + if (line < enclosingLine || (line === enclosingLine && col < enclosingCol)) {
2270 + // Protection against invalid enclosing information. Should not happen.
2271 + enclosingLine = 0;
2272 + enclosingCol = 0;
2273 + }
2274 + if (line < 1) {
2275 + // Fit everything on the first line.
2276 + const minCol = encodedName.length + 3;
2277 + let enclosingColDistance = enclosingCol - minCol;
2278 + if (enclosingColDistance < 0) {
2279 + enclosingColDistance = 0;
2280 + }
2281 + let colDistance = col - enclosingColDistance - minCol - 3;
2282 + if (colDistance < 0) {
2283 + colDistance = 0;
2284 + }
2285 code =
2286 '({' +
2287 encodedName +
2251 - ':_=>' +
2252 - ' '.repeat(col < minSize ? 0 : col - minSize) +
2253 - '_()})\n' +
2254 - comment;
2288 + ':' +
2289 + ' '.repeat(enclosingColDistance) +
2290 + '_=>' +
2291 + ' '.repeat(colDistance) +
2292 + '_()})';
2293 + } else if (enclosingLine < 1) {
2294 + // Fit just the enclosing function on the first line.
2295 + const minCol = encodedName.length + 3;
2296 + let enclosingColDistance = enclosingCol - minCol;
2297 + if (enclosingColDistance < 0) {
2298 + enclosingColDistance = 0;
2299 + }
2300 + code =
2301 + '({' +
2302 + encodedName +
2303 + ':' +
2304 + ' '.repeat(enclosingColDistance) +
2305 + '_=>' +
2306 + '\n'.repeat(line - enclosingLine) +
2307 + ' '.repeat(col) +
2308 + '_()})';
2309 + } else if (enclosingLine === line) {
2310 + // Fit the enclosing function and callsite on same line.
2311 + let colDistance = col - enclosingCol - 3;
2312 + if (colDistance < 0) {
2313 + colDistance = 0;
2314 + }
2315 + code =
2316 + '\n'.repeat(enclosingLine - 1) +
2317 + '({' +
2318 + encodedName +
2319 + ':\n' +
2320 + ' '.repeat(enclosingCol) +
2321 + '_=>' +
2322 + ' '.repeat(colDistance) +
2323 + '_()})';
2324 } else {
2325 + // This is the ideal because we can always encode any position.
2326 code =
2257 - comment +
2258 - '\n'.repeat(line - 2) +
2327 + '\n'.repeat(enclosingLine - 1) +
2328 '({' +
2329 encodedName +
2261 - ':_=>\n' +
2262 - ' '.repeat(col < 1 ? 0 : col - 1) +
2330 + ':\n' +
2331 + ' '.repeat(enclosingCol) +
2332 + '_=>' +
2333 + '\n'.repeat(line - enclosingLine) +
2334 + ' '.repeat(col) +
2335 '_()})';
2336 }
2337
2338 + if (enclosingLine < 1) {
2339 + // If the function starts at the first line, we append the comment after.
2340 + code = code + '\n' + comment;
2341 + } else {
2342 + // Otherwise we prepend the comment on the first line.
2343 + code = comment + code;
2344 + }
2345 +
2346 if (filename.startsWith('/')) {
2347 // If the filename starts with `/` we assume that it is a file system file
2348 // rather than relative to the current host. Since on the server fully qualified
@@ -2320,7 +2400,7 @@ function buildFakeCallStack<T>(
2400 const frameKey = frame.join('-') + '-' + environmentName;
2401 let fn = fakeFunctionCache.get(frameKey);
2402 if (fn === undefined) {
2323 - const [name, filename, line, col] = frame;
2403 + const [name, filename, line, col, enclosingLine, enclosingCol] = frame;
2404 const findSourceMapURL = response._debugFindSourceMapURL;
2405 const sourceMap = findSourceMapURL
2406 ? findSourceMapURL(filename, environmentName)
@@ -2331,6 +2411,8 @@ function buildFakeCallStack<T>(
2411 sourceMap,
2412 line,
2413 col,
2414 + enclosingLine,
2415 + enclosingCol,
2416 environmentName,
2417 );
2418 // TODO: This cache should technically live on the response since the _debugFindSourceMapURL
packages/react-client/src/ReactFlightReplyClient.js
+2 -1
@@ -1350,10 +1350,11 @@ function parseStackLocation(error: Error): null | ReactCallSite {
1350 if (filename === '<anonymous>') {
1351 filename = '';
1352 }
1353 + // This is really the enclosingLine/Column.
1354 const line = +(parsed[3] || parsed[6]);
1355 const col = +(parsed[4] || parsed[7]);
1356
1356 - return [name, filename, line, col];
1357 + return [name, filename, line, col, line, col];
1358 }
1359
1360 export function createServerReference<A: Iterable<any>, T>(
packages/react-server/src/ReactFlightStackConfigV8.js
+13 -3
@@ -63,7 +63,7 @@ function collectStackTrace(
63 // Skip everything after the bottom frame since it'll be internals.
64 break;
65 } else if (callSite.isNative()) {
66 - result.push([name, '', 0, 0]);
66 + result.push([name, '', 0, 0, 0, 0]);
67 } else {
68 // We encode complex function calls as if they're part of the function
69 // name since we cannot simulate the complex ones and they look the same
@@ -88,7 +88,17 @@ function collectStackTrace(
88 }
89 const line = callSite.getLineNumber() || 0;
90 const col = callSite.getColumnNumber() || 0;
91 - result.push([name, filename, line, col]);
91 + const enclosingLine: number =
92 + // $FlowFixMe[prop-missing]
93 + typeof callSite.getEnclosingLineNumber === 'function'
94 + ? (callSite: any).getEnclosingLineNumber() || 0
95 + : 0;
96 + const enclosingCol: number =
97 + // $FlowFixMe[prop-missing]
98 + typeof callSite.getEnclosingColumnNumber === 'function'
99 + ? (callSite: any).getEnclosingColumnNumber() || 0
100 + : 0;
101 + result.push([name, filename, line, col, enclosingLine, enclosingCol]);
102 }
103 }
104 // At the same time we generate a string stack trace just in case someone
@@ -179,7 +189,7 @@ export function parseStackTrace(
189 }
190 const line = +(parsed[3] || parsed[6]);
191 const col = +(parsed[4] || parsed[7]);
182 - parsedFrames.push([name, filename, line, col]);
192 + parsedFrames.push([name, filename, line, col, 0, 0]);
193 }
194 return parsedFrames;
195 }
packages/shared/ReactTypes.js
+2
@@ -186,6 +186,8 @@ export type ReactCallSite = [
186 string, // file name TODO: model nested eval locations as nested arrays
187 number, // line number
188 number, // column number
189 + number, // enclosing line number
190 + number, // enclosing column number
191 ];
192
193 export type ReactStackTrace = Array<ReactCallSite>;