@samitouri / QOS-React-2 / commits / cdbafc8e67

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: c2015bb5565746d07427587526b71e23685279c2 Pull Request resolved: https://github.com/facebook/react/pull/29810

Joe Savona committed Jun 7, 2024 at 16:44 UTC cdbafc8e67bb0669e70d361a956ab56ccd3bf7a3
8 files changed +240 -13
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts
+3
@@ -169,6 +169,9 @@ export type LoggerEvent =
169 fnName: string | null;
170 memoSlots: number;
171 memoBlocks: number;
172 + memoValues: number;
173 + prunedMemoBlocks: number;
174 + prunedMemoValues: number;
175 }
176 | {
177 kind: "PipelineError";
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+3
@@ -329,6 +329,9 @@ export function compileProgram(
329 fnName: compiledFn.id?.name ?? null,
330 memoSlots: compiledFn.memoSlotsUsed,
331 memoBlocks: compiledFn.memoBlocks,
332 + memoValues: compiledFn.memoValues,
333 + prunedMemoBlocks: compiledFn.prunedMemoBlocks,
334 + prunedMemoValues: compiledFn.prunedMemoValues,
335 });
336 } catch (err) {
337 hasCriticalError ||= isCriticalError(err);
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts
+67 -6
@@ -22,6 +22,7 @@ import {
22 ObjectPropertyKey,
23 Pattern,
24 Place,
25 + PrunedReactiveScopeBlock,
26 ReactiveBlock,
27 ReactiveFunction,
28 ReactiveInstruction,
@@ -67,6 +68,23 @@ export type CodegenFunction = {
68 * how many inputs/outputs each block has
69 */
70 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;
88 };
89
90 export function codegenFunction(
@@ -272,7 +290,7 @@ function codegenReactiveFunction(
290 return Err(cx.errors);
291 }
292
275 - const countMemoBlockVisitor = new CountMemoBlockVisitor();
293 + const countMemoBlockVisitor = new CountMemoBlockVisitor(fn.env);
294 visitReactiveFunction(fn, countMemoBlockVisitor, undefined);
295
296 return Ok({
@@ -284,16 +302,59 @@ function codegenReactiveFunction(
302 generator: fn.generator,
303 async: fn.async,
304 memoSlotsUsed: cx.nextCacheIndex,
287 - memoBlocks: countMemoBlockVisitor.count,
305 + memoBlocks: countMemoBlockVisitor.memoBlocks,
306 + memoValues: countMemoBlockVisitor.memoValues,
307 + prunedMemoBlocks: countMemoBlockVisitor.prunedMemoBlocks,
308 + prunedMemoValues: countMemoBlockVisitor.prunedMemoValues,
309 });
310 }
311
312 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;
318
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);
358 }
359 }
360
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/log-pruned-memoization.expect.md new
+94
@@ -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}]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/log-pruned-memoization.js new
+31
@@ -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
+32 -7
@@ -14,6 +14,8 @@ import * as t from "@babel/types";
14 import assert from "assert";
15 import type {
16 CompilationMode,
17 + Logger,
18 + LoggerEvent,
19 PanicThresholdOptions,
20 PluginOptions,
21 } from "babel-plugin-react-compiler/src/Entrypoint";
@@ -32,7 +34,7 @@ export function parseLanguage(source: string): "flow" | "typescript" {
34 function makePluginOptions(
35 firstLine: string,
36 parseConfigPragmaFn: typeof ParseConfigPragma
35 -): PluginOptions {
37 +): [PluginOptions, Array<{ filename: string | null; event: LoggerEvent }>] {
38 let gating = null;
39 let enableEmitInstrumentForget = null;
40 let enableEmitFreeze = null;
@@ -140,8 +142,18 @@ function makePluginOptions(
142 );
143 }
144
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 +
155 const config = parseConfigPragmaFn(firstLine);
144 - return {
156 + const options = {
157 environment: {
158 ...config,
159 customHooks: new Map([
@@ -183,7 +195,7 @@ function makePluginOptions(
195 enableChangeDetectionForDebugging,
196 },
197 compilationMode,
186 - logger: null,
198 + logger,
199 gating,
200 panicThreshold,
201 noEmit: false,
@@ -193,6 +205,7 @@ function makePluginOptions(
205 ignoreUseNoForget,
206 enableReanimatedCheck: false,
207 };
208 + return [options, logs];
209 }
210
211 export function parseInput(
@@ -294,6 +307,7 @@ const FlowEvaluatorPresets = getEvaluatorPresets("flow");
307
308 export type TransformResult = {
309 forgetOutput: string;
310 + logs: string | null;
311 evaluatorCode: {
312 original: string;
313 forget: string;
@@ -330,12 +344,13 @@ export async function transformFixtureInput(
344 /**
345 * Get Forget compiled code
346 */
347 + const [options, logs] = makePluginOptions(firstLine, parseConfigPragmaFn);
348 const forgetResult = transformFromAstSync(inputAst, input, {
349 filename: virtualFilepath,
350 highlightCode: false,
351 retainLines: true,
352 plugins: [
338 - [plugin, makePluginOptions(firstLine, parseConfigPragmaFn)],
353 + [plugin, options],
354 "babel-plugin-fbt",
355 "babel-plugin-fbt-runtime",
356 ],
@@ -349,7 +364,7 @@ export async function transformFixtureInput(
364 forgetResult?.code != null,
365 "Expected BabelPluginReactForget to codegen successfully."
366 );
352 - const forgetOutput = forgetResult.code;
367 + const forgetCode = forgetResult.code;
368 let evaluatorCode = null;
369
370 if (
@@ -363,7 +378,7 @@ export async function transformFixtureInput(
378 forgetResult?.ast != null,
379 "Expected BabelPluginReactForget ast."
380 );
366 - const result = transformFromAstSync(forgetResult.ast, forgetOutput, {
381 + const result = transformFromAstSync(forgetResult.ast, forgetCode, {
382 presets,
383 filename: virtualFilepath,
384 configFile: false,
@@ -415,10 +430,20 @@ export async function transformFixtureInput(
430 original: originalEval,
431 };
432 }
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 + }
442 return {
443 kind: "ok",
444 value: {
421 - forgetOutput: await format(forgetOutput, language),
445 + forgetOutput,
446 + logs: formattedLogs,
447 evaluatorCode,
448 },
449 };
compiler/packages/snap/src/reporter.ts
+9
@@ -22,6 +22,7 @@ export function writeOutputToString(
22 input: string,
23 compilerOutput: string | null,
24 evaluatorOutput: string | null,
25 + logs: string | null,
26 errorMessage: string | null
27 ) {
28 // leading newline intentional
@@ -41,6 +42,14 @@ ${wrapWithTripleBackticks(compilerOutput, "javascript")}
42 result += "\n";
43 }
44
45 + if (logs != null) {
46 + result += `
47 +## Logs
48 +
49 +${wrapWithTripleBackticks(logs, null)}
50 +`;
51 + }
52 +
53 if (errorMessage != null) {
54 result += `
55 ## Error
compiler/packages/snap/src/runner-worker.ts
+1
@@ -189,6 +189,7 @@ export async function transformFixture(
189 input,
190 snapOutput,
191 sproutOutput,
192 + compileResult?.logs ?? null,
193 error
194 );
195