Skip to content

Commit 4fa8fa7

Browse files
committed
compiler: Log metrics on pruned memo blocks/values
Adds additional information to the CompileSuccess LoggerEvent: * `prunedMemoBlocks` is the number of reactive scopes that were pruned for some reason. * `prunedMemoValues` is the number of unique _values_ produced by those scopes. Both numbers exclude blocks that are just a hook call - ie although we create and prune a scope for eg `useState()`, that's just an artifact of the sequencing of our pipeline. So what this metric is counting is cases of _other_ values that go unmemoized. See the new fixture, which takes advantage of improvements in the snap runner to optionally emit the logger events in the .expect.md file if you include the "logger" pragma in a fixture. ghstack-source-id: c2015bb Pull Request resolved: #29810
1 parent 6fbf533 commit 4fa8fa7

8 files changed

Lines changed: 240 additions & 13 deletions

File tree

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ export type LoggerEvent =
169169
fnName: string | null;
170170
memoSlots: number;
171171
memoBlocks: number;
172+
memoValues: number;
173+
prunedMemoBlocks: number;
174+
prunedMemoValues: number;
172175
}
173176
| {
174177
kind: "PipelineError";

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ export function compileProgram(
329329
fnName: compiledFn.id?.name ?? null,
330330
memoSlots: compiledFn.memoSlotsUsed,
331331
memoBlocks: compiledFn.memoBlocks,
332+
memoValues: compiledFn.memoValues,
333+
prunedMemoBlocks: compiledFn.prunedMemoBlocks,
334+
prunedMemoValues: compiledFn.prunedMemoValues,
332335
});
333336
} catch (err) {
334337
hasCriticalError ||= isCriticalError(err);

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
ObjectPropertyKey,
2323
Pattern,
2424
Place,
25+
PrunedReactiveScopeBlock,
2526
ReactiveBlock,
2627
ReactiveFunction,
2728
ReactiveInstruction,
@@ -67,6 +68,23 @@ export type CodegenFunction = {
6768
* how many inputs/outputs each block has
6869
*/
6970
memoBlocks: number;
71+
72+
/**
73+
* Number of memoized values across all reactive scopes
74+
*/
75+
memoValues: number;
76+
77+
/**
78+
* The number of reactive scopes that were created but had to be discarded
79+
* because they contained hook calls.
80+
*/
81+
prunedMemoBlocks: number;
82+
83+
/**
84+
* The total number of values that should have been memoized but weren't
85+
* because they were part of a pruned memo block.
86+
*/
87+
prunedMemoValues: number;
7088
};
7189

7290
export function codegenFunction(
@@ -272,7 +290,7 @@ function codegenReactiveFunction(
272290
return Err(cx.errors);
273291
}
274292

275-
const countMemoBlockVisitor = new CountMemoBlockVisitor();
293+
const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
276294
visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
277295

278296
return Ok({
@@ -284,16 +302,59 @@ function codegenReactiveFunction(
284302
generator: fn.generator,
285303
async: fn.async,
286304
memoSlotsUsed: cx.nextCacheIndex,
287-
memoBlocks: countMemoBlockVisitor.count,
305+
memoBlocks: countMemoBlockVisitor.memoBlocks,
306+
memoValues: countMemoBlockVisitor.memoValues,
307+
prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
308+
prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
288309
});
289310
}
290311

291312
class CountMemoBlockVisitor extends ReactiveFunctionVisitor<void> {
292-
count: number = 0;
313+
env: Environment;
314+
memoBlocks: number = 0;
315+
memoValues: number = 0;
316+
prunedMemoBlocks: number = 0;
317+
prunedMemoValues: number = 0;
293318

294-
override visitScope(scope: ReactiveScopeBlock, state: void): void {
295-
this.count += 1;
296-
this.traverseScope(scope, state);
319+
constructor(env: Environment) {
320+
super();
321+
this.env = env;
322+
}
323+
324+
override visitScope(scopeBlock: ReactiveScopeBlock, state: void): void {
325+
this.memoBlocks += 1;
326+
this.memoValues += scopeBlock.scope.declarations.size;
327+
this.traverseScope(scopeBlock, state);
328+
}
329+
330+
override visitPrunedScope(
331+
scopeBlock: PrunedReactiveScopeBlock,
332+
state: void
333+
): void {
334+
let isHookOnlyMemoBlock = false;
335+
if (
336+
scopeBlock.instructions.length === 1 &&
337+
scopeBlock.instructions[0].kind === "instruction"
338+
) {
339+
const instr = scopeBlock.instructions[0]!.instruction;
340+
if (
341+
instr.value.kind === "MethodCall" ||
342+
instr.value.kind === "CallExpression"
343+
) {
344+
const callee =
345+
instr.value.kind === "MethodCall"
346+
? instr.value.property
347+
: instr.value.callee;
348+
if (getHookKind(this.env, callee.identifier) != null) {
349+
isHookOnlyMemoBlock = true;
350+
}
351+
}
352+
}
353+
if (!isHookOnlyMemoBlock) {
354+
this.prunedMemoBlocks += 1;
355+
this.prunedMemoValues += scopeBlock.scope.declarations.size;
356+
}
357+
this.traversePrunedScope(scopeBlock, state);
297358
}
298359
}
299360

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @logger
6+
import { useState } from "react";
7+
import { identity, makeObject_Primitives, useHook } from "shared-runtime";
8+
9+
function Component() {
10+
// The scopes for x and x2 are interleaved, so this is one scope with two values
11+
const x = makeObject_Primitives();
12+
const x2 = makeObject_Primitives();
13+
useState(null);
14+
identity(x);
15+
identity(x2);
16+
17+
// We create a scope for all call expressions, but prune those with hook calls
18+
// in this case it's _just_ a hook call, so we don't count this as pruned
19+
const y = useHook();
20+
21+
const z = [];
22+
for (let i = 0; i < 10; i++) {
23+
// The scope for obj is pruned bc it's in a loop
24+
const obj = makeObject_Primitives();
25+
z.push(obj);
26+
}
27+
28+
// Overall we expect two pruned scopes (for x+x2, and obj), with 3 pruned scope values.
29+
return [x, x2, y, z];
30+
}
31+
32+
export const FIXTURE_ENTRYPOINT = {
33+
fn: Component,
34+
params: [{}],
35+
};
36+
37+
```
38+
39+
## Code
40+
41+
```javascript
42+
import { c as _c } from "react/compiler-runtime"; // @logger
43+
import { useState } from "react";
44+
import { identity, makeObject_Primitives, useHook } from "shared-runtime";
45+
46+
function Component() {
47+
const $ = _c(5);
48+
49+
const x = makeObject_Primitives();
50+
const x2 = makeObject_Primitives();
51+
useState(null);
52+
identity(x);
53+
identity(x2);
54+
55+
const y = useHook();
56+
let z;
57+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
58+
z = [];
59+
for (let i = 0; i < 10; i++) {
60+
const obj = makeObject_Primitives();
61+
z.push(obj);
62+
}
63+
$[0] = z;
64+
} else {
65+
z = $[0];
66+
}
67+
let t0;
68+
if ($[1] !== x || $[2] !== x2 || $[3] !== y) {
69+
t0 = [x, x2, y, z];
70+
$[1] = x;
71+
$[2] = x2;
72+
$[3] = y;
73+
$[4] = t0;
74+
} else {
75+
t0 = $[4];
76+
}
77+
return t0;
78+
}
79+
80+
export const FIXTURE_ENTRYPOINT = {
81+
fn: Component,
82+
params: [{}],
83+
};
84+
85+
```
86+
87+
## Logs
88+
89+
```
90+
{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":121},"end":{"line":26,"column":1,"index":813},"filename":"log-pruned-memoization.ts"},"fnName":"Component","memoSlots":5,"memoBlocks":2,"memoValues":2,"prunedMemoBlocks":2,"prunedMemoValues":3}
91+
```
92+
93+
### Eval output
94+
(kind: ok) [{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},[{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true},{"a":0,"b":"value1","c":true}]]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// @logger
2+
import { useState } from "react";
3+
import { identity, makeObject_Primitives, useHook } from "shared-runtime";
4+
5+
function Component() {
6+
// The scopes for x and x2 are interleaved, so this is one scope with two values
7+
const x = makeObject_Primitives();
8+
const x2 = makeObject_Primitives();
9+
useState(null);
10+
identity(x);
11+
identity(x2);
12+
13+
// We create a scope for all call expressions, but prune those with hook calls
14+
// in this case it's _just_ a hook call, so we don't count this as pruned
15+
const y = useHook();
16+
17+
const z = [];
18+
for (let i = 0; i < 10; i++) {
19+
// The scope for obj is pruned bc it's in a loop
20+
const obj = makeObject_Primitives();
21+
z.push(obj);
22+
}
23+
24+
// Overall we expect two pruned scopes (for x+x2, and obj), with 3 pruned scope values.
25+
return [x, x2, y, z];
26+
}
27+
28+
export const FIXTURE_ENTRYPOINT = {
29+
fn: Component,
30+
params: [{}],
31+
};

compiler/packages/snap/src/compiler.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import * as t from "@babel/types";
1414
import assert from "assert";
1515
import type {
1616
CompilationMode,
17+
Logger,
18+
LoggerEvent,
1719
PanicThresholdOptions,
1820
PluginOptions,
1921
} from "babel-plugin-react-compiler/src/Entrypoint";
@@ -32,7 +34,7 @@ export function parseLanguage(source: string): "flow" | "typescript" {
3234
function makePluginOptions(
3335
firstLine: string,
3436
parseConfigPragmaFn: typeof ParseConfigPragma
35-
): PluginOptions {
37+
): [PluginOptions, Array<{ filename: string | null; event: LoggerEvent }>] {
3638
let gating = null;
3739
let enableEmitInstrumentForget = null;
3840
let enableEmitFreeze = null;
@@ -140,8 +142,18 @@ function makePluginOptions(
140142
);
141143
}
142144

145+
let logs: Array<{ filename: string | null; event: LoggerEvent }> = [];
146+
let logger: Logger | null = null;
147+
if (firstLine.includes("@logger")) {
148+
logger = {
149+
logEvent(filename: string | null, event: LoggerEvent): void {
150+
logs.push({ filename, event });
151+
},
152+
};
153+
}
154+
143155
const config = parseConfigPragmaFn(firstLine);
144-
return {
156+
const options = {
145157
environment: {
146158
...config,
147159
customHooks: new Map([
@@ -183,7 +195,7 @@ function makePluginOptions(
183195
enableChangeDetectionForDebugging,
184196
},
185197
compilationMode,
186-
logger: null,
198+
logger,
187199
gating,
188200
panicThreshold,
189201
noEmit: false,
@@ -193,6 +205,7 @@ function makePluginOptions(
193205
ignoreUseNoForget,
194206
enableReanimatedCheck: false,
195207
};
208+
return [options, logs];
196209
}
197210

198211
export function parseInput(
@@ -294,6 +307,7 @@ const FlowEvaluatorPresets = getEvaluatorPresets("flow");
294307

295308
export type TransformResult = {
296309
forgetOutput: string;
310+
logs: string | null;
297311
evaluatorCode: {
298312
original: string;
299313
forget: string;
@@ -330,12 +344,13 @@ export async function transformFixtureInput(
330344
/**
331345
* Get Forget compiled code
332346
*/
347+
const [options, logs] = makePluginOptions(firstLine, parseConfigPragmaFn);
333348
const forgetResult = transformFromAstSync(inputAst, input, {
334349
filename: virtualFilepath,
335350
highlightCode: false,
336351
retainLines: true,
337352
plugins: [
338-
[plugin, makePluginOptions(firstLine, parseConfigPragmaFn)],
353+
[plugin, options],
339354
"babel-plugin-fbt",
340355
"babel-plugin-fbt-runtime",
341356
],
@@ -349,7 +364,7 @@ export async function transformFixtureInput(
349364
forgetResult?.code != null,
350365
"Expected BabelPluginReactForget to codegen successfully."
351366
);
352-
const forgetOutput = forgetResult.code;
367+
const forgetCode = forgetResult.code;
353368
let evaluatorCode = null;
354369

355370
if (
@@ -363,7 +378,7 @@ export async function transformFixtureInput(
363378
forgetResult?.ast != null,
364379
"Expected BabelPluginReactForget ast."
365380
);
366-
const result = transformFromAstSync(forgetResult.ast, forgetOutput, {
381+
const result = transformFromAstSync(forgetResult.ast, forgetCode, {
367382
presets,
368383
filename: virtualFilepath,
369384
configFile: false,
@@ -415,10 +430,20 @@ export async function transformFixtureInput(
415430
original: originalEval,
416431
};
417432
}
433+
const forgetOutput = await format(forgetCode, language);
434+
let formattedLogs = null;
435+
if (logs.length !== 0) {
436+
formattedLogs = logs
437+
.map(({ event }) => {
438+
return JSON.stringify(event);
439+
})
440+
.join("\n");
441+
}
418442
return {
419443
kind: "ok",
420444
value: {
421-
forgetOutput: await format(forgetOutput, language),
445+
forgetOutput,
446+
logs: formattedLogs,
422447
evaluatorCode,
423448
},
424449
};

compiler/packages/snap/src/reporter.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function writeOutputToString(
2222
input: string,
2323
compilerOutput: string | null,
2424
evaluatorOutput: string | null,
25+
logs: string | null,
2526
errorMessage: string | null
2627
) {
2728
// leading newline intentional
@@ -41,6 +42,14 @@ ${wrapWithTripleBackticks(compilerOutput, "javascript")}
4142
result += "\n";
4243
}
4344

45+
if (logs != null) {
46+
result += `
47+
## Logs
48+
49+
${wrapWithTripleBackticks(logs, null)}
50+
`;
51+
}
52+
4453
if (errorMessage != null) {
4554
result += `
4655
## Error

0 commit comments

Comments
 (0)