mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[other] Change instrumentation to use an optional gating identifier; record
filepath
Internal rollout currently has a good number of test failures.
`enableEmitInstrumentForget` can help developers understand which functions /
files they should look at:
```
// input
function Foo() {
userCode();
// ...
}
// output
function Foo() {
if (__DEV__ && inE2eTestMode) {
logRender("Foo", "/path/to/filename.js");
}
const $ = useMemoCache(...);
userCode();
}
```
This commit is contained in:
@@ -176,7 +176,7 @@ function compile(source: string): CompilerOutput {
|
||||
for (const result of run(fn, {
|
||||
...config,
|
||||
customHooks: new Map([...COMMON_HOOKS]),
|
||||
})) {
|
||||
}, null)) {
|
||||
const fnName = fn.node.id?.name ?? null;
|
||||
switch (result.kind) {
|
||||
case "ast": {
|
||||
|
||||
@@ -168,7 +168,7 @@ function ReactForgetFunctionTransform() {
|
||||
}
|
||||
}
|
||||
|
||||
const compiled = compile(fn, forgetOptions);
|
||||
const compiled = compile(fn, forgetOptions, null);
|
||||
compiledFns.add(compiled);
|
||||
|
||||
const fun = t.functionDeclaration(
|
||||
|
||||
@@ -89,7 +89,8 @@ export function* run(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
config: EnvironmentConfig
|
||||
config: EnvironmentConfig,
|
||||
filename: string | null
|
||||
): Generator<CompilerPipelineValue, CodegenFunction> {
|
||||
const contextIdentifiers = findContextIdentifiers(func);
|
||||
const env = new Environment(config, contextIdentifiers);
|
||||
@@ -98,7 +99,7 @@ export function* run(
|
||||
name: "EnvironmentConfig",
|
||||
value: prettyFormat(env.config),
|
||||
};
|
||||
const ast = yield* runWithEnvironment(func, env);
|
||||
const ast = yield* runWithEnvironment(func, env, filename);
|
||||
return ast;
|
||||
}
|
||||
|
||||
@@ -110,7 +111,8 @@ function* runWithEnvironment(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
env: Environment
|
||||
env: Environment,
|
||||
filename: string | null
|
||||
): Generator<CompilerPipelineValue, CodegenFunction> {
|
||||
const hir = lower(func, env).unwrap();
|
||||
yield log({ kind: "hir", name: "HIR", value: hir });
|
||||
@@ -358,7 +360,7 @@ function* runWithEnvironment(
|
||||
validatePreservedManualMemoization(reactiveFunction);
|
||||
}
|
||||
|
||||
const ast = codegenFunction(reactiveFunction).unwrap();
|
||||
const ast = codegenFunction(reactiveFunction, filename).unwrap();
|
||||
yield log({ kind: "ast", name: "Codegen", value: ast });
|
||||
|
||||
/**
|
||||
@@ -377,9 +379,10 @@ export function compileFn(
|
||||
func: NodePath<
|
||||
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
|
||||
>,
|
||||
config: EnvironmentConfig
|
||||
config: EnvironmentConfig,
|
||||
filename: string | null
|
||||
): CodegenFunction {
|
||||
let generator = run(func, config);
|
||||
let generator = run(func, config, filename);
|
||||
while (true) {
|
||||
const next = generator.next();
|
||||
if (next.done) {
|
||||
|
||||
@@ -252,9 +252,17 @@ export function compileProgram(
|
||||
* TODO(lauren): Remove pass.opts.environment nullcheck once PluginOptions
|
||||
* is validated
|
||||
*/
|
||||
if (environment.isErr()) {
|
||||
CompilerError.throwInvalidConfig({
|
||||
reason: "Error in validating environment config",
|
||||
description: environment.unwrapErr().toString(),
|
||||
suggestions: null,
|
||||
loc: null,
|
||||
});
|
||||
}
|
||||
const config = environment.unwrap();
|
||||
|
||||
compiledFn = compileFn(fn, config);
|
||||
compiledFn = compileFn(fn, config, pass.filename);
|
||||
pass.opts.logger?.logEvent(pass.filename, {
|
||||
kind: "CompileSuccess",
|
||||
fnLoc: fn.node.loc ?? null,
|
||||
@@ -319,7 +327,6 @@ export function compileProgram(
|
||||
}
|
||||
|
||||
const externalFunctions: ExternalFunction[] = [];
|
||||
let instrumentForget: null | ExternalFunction = null;
|
||||
let gating: null | ExternalFunction = null;
|
||||
try {
|
||||
// TODO: check for duplicate import specifiers
|
||||
@@ -328,11 +335,17 @@ export function compileProgram(
|
||||
externalFunctions.push(gating);
|
||||
}
|
||||
|
||||
if (options.environment?.enableEmitInstrumentForget != null) {
|
||||
instrumentForget = tryParseExternalFunction(
|
||||
options.environment.enableEmitInstrumentForget
|
||||
const enableEmitInstrumentForget =
|
||||
options.environment?.enableEmitInstrumentForget;
|
||||
if (enableEmitInstrumentForget != null) {
|
||||
externalFunctions.push(
|
||||
tryParseExternalFunction(enableEmitInstrumentForget.fn)
|
||||
);
|
||||
externalFunctions.push(instrumentForget);
|
||||
if (enableEmitInstrumentForget.gating != null) {
|
||||
externalFunctions.push(
|
||||
tryParseExternalFunction(enableEmitInstrumentForget.gating)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.environment?.enableEmitFreeze != null) {
|
||||
|
||||
@@ -45,6 +45,12 @@ export const ExternalFunctionSchema = z.object({
|
||||
// Unique name for the feature flag test condition, eg `isForgetEnabled_ProjectName`
|
||||
importSpecifierName: z.string(),
|
||||
});
|
||||
|
||||
export const InstrumentationSchema = z.object({
|
||||
fn: ExternalFunctionSchema,
|
||||
gating: ExternalFunctionSchema.nullish(),
|
||||
});
|
||||
|
||||
export type ExternalFunction = z.infer<typeof ExternalFunctionSchema>;
|
||||
|
||||
const HookSchema = z.object({
|
||||
@@ -254,22 +260,24 @@ const EnvironmentConfigSchema = z.object({
|
||||
* instrumentation function, for components and hooks that Forget compiles.
|
||||
* For example:
|
||||
* instrumentForget: {
|
||||
* source: 'react-forget-runtime',
|
||||
* importSpecifierName: 'useRenderCounter',
|
||||
* import: {
|
||||
* source: 'react-forget-runtime',
|
||||
* importSpecifierName: 'useRenderCounter',
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* produces:
|
||||
* import {useRenderCounter} from 'react-forget-runtime-pokes';
|
||||
* import {useRenderCounter} from 'react-forget-runtime';
|
||||
*
|
||||
* function Component(props) {
|
||||
* if (__DEV__) {
|
||||
* useRenderCounter();
|
||||
* useRenderCounter("Component", "/filepath/filename.js");
|
||||
* }
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
*/
|
||||
enableEmitInstrumentForget: ExternalFunctionSchema.nullish(),
|
||||
enableEmitInstrumentForget: InstrumentationSchema.nullish(),
|
||||
|
||||
/**
|
||||
* Enable support for reactive scopes that contain an early return.
|
||||
|
||||
+16
-5
@@ -72,7 +72,8 @@ export type CodegenFunction = {
|
||||
};
|
||||
|
||||
export function codegenFunction(
|
||||
fn: ReactiveFunction
|
||||
fn: ReactiveFunction,
|
||||
filename: string | null
|
||||
): Result<CodegenFunction, CompilerError> {
|
||||
const cx = new Context(fn.env, fn.id ?? "[[ anonymous ]]", null);
|
||||
const compileResult = codegenReactiveFunction(cx, fn);
|
||||
@@ -112,14 +113,24 @@ export function codegenFunction(
|
||||
if (emitInstrumentForget != null && fn.id != null) {
|
||||
/*
|
||||
* Technically, this is a conditional hook call. However, we expect
|
||||
* __DEV__ and gatingIdentifier to be runtime constants
|
||||
* __DEV__ and gating identifier to be runtime constants
|
||||
*/
|
||||
let gating: t.Expression;
|
||||
if (emitInstrumentForget.gating != null) {
|
||||
gating = t.logicalExpression(
|
||||
"&&",
|
||||
t.identifier("__DEV__"),
|
||||
t.identifier(emitInstrumentForget.gating.importSpecifierName)
|
||||
);
|
||||
} else {
|
||||
gating = t.identifier("__DEV__");
|
||||
}
|
||||
const test: t.IfStatement = t.ifStatement(
|
||||
t.identifier("__DEV__"),
|
||||
gating,
|
||||
t.expressionStatement(
|
||||
t.callExpression(
|
||||
t.identifier(emitInstrumentForget.importSpecifierName),
|
||||
[t.stringLiteral(fn.id)]
|
||||
t.identifier(emitInstrumentForget.fn.importSpecifierName),
|
||||
[t.stringLiteral(fn.id), t.stringLiteral(filename ?? "")]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
+7
-2
@@ -13,11 +13,16 @@ function useFoo(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { useRenderCounter, makeReadOnly } from "react-forget-runtime";
|
||||
import {
|
||||
useRenderCounter,
|
||||
shouldInstrument,
|
||||
makeReadOnly,
|
||||
} from "react-forget-runtime";
|
||||
import { unstable_useMemoCache as useMemoCache } from "react"; // @enableEmitFreeze @instrumentForget
|
||||
|
||||
function useFoo(props) {
|
||||
if (__DEV__) useRenderCounter("useFoo");
|
||||
if (__DEV__ && shouldInstrument)
|
||||
useRenderCounter("useFoo", "/codegen-emit-imports-same-source.ts");
|
||||
const $ = useMemoCache(2);
|
||||
let t0;
|
||||
if ($[0] !== props.x) {
|
||||
|
||||
+5
-3
@@ -24,11 +24,12 @@ function Foo(props) {
|
||||
|
||||
```javascript
|
||||
import { isForgetEnabled_Fixtures } from "ReactForgetFeatureFlag";
|
||||
import { useRenderCounter } from "react-forget-runtime";
|
||||
import { useRenderCounter, shouldInstrument } from "react-forget-runtime";
|
||||
import { unstable_useMemoCache as useMemoCache } from "react"; // @instrumentForget @compilationMode(annotation) @gating
|
||||
const Bar = isForgetEnabled_Fixtures()
|
||||
? function Bar(props) {
|
||||
if (__DEV__) useRenderCounter("Bar");
|
||||
if (__DEV__ && shouldInstrument)
|
||||
useRenderCounter("Bar", "/codegen-instrument-forget-gating-test.ts");
|
||||
const $ = useMemoCache(2);
|
||||
let t0;
|
||||
if ($[0] !== props.bar) {
|
||||
@@ -50,7 +51,8 @@ function NoForget(props) {
|
||||
}
|
||||
const Foo = isForgetEnabled_Fixtures()
|
||||
? function Foo(props) {
|
||||
if (__DEV__) useRenderCounter("Foo");
|
||||
if (__DEV__ && shouldInstrument)
|
||||
useRenderCounter("Foo", "/codegen-instrument-forget-gating-test.ts");
|
||||
const $ = useMemoCache(2);
|
||||
let t0;
|
||||
if ($[0] !== props.bar) {
|
||||
|
||||
+5
-3
@@ -23,11 +23,12 @@ function Foo(props) {
|
||||
## Code
|
||||
|
||||
```javascript
|
||||
import { useRenderCounter } from "react-forget-runtime";
|
||||
import { useRenderCounter, shouldInstrument } from "react-forget-runtime";
|
||||
import { unstable_useMemoCache as useMemoCache } from "react"; // @instrumentForget @compilationMode(annotation)
|
||||
|
||||
function Bar(props) {
|
||||
if (__DEV__) useRenderCounter("Bar");
|
||||
if (__DEV__ && shouldInstrument)
|
||||
useRenderCounter("Bar", "/codegen-instrument-forget-test.ts");
|
||||
const $ = useMemoCache(2);
|
||||
let t0;
|
||||
if ($[0] !== props.bar) {
|
||||
@@ -45,7 +46,8 @@ function NoForget(props) {
|
||||
}
|
||||
|
||||
function Foo(props) {
|
||||
if (__DEV__) useRenderCounter("Foo");
|
||||
if (__DEV__ && shouldInstrument)
|
||||
useRenderCounter("Foo", "/codegen-instrument-forget-test.ts");
|
||||
const $ = useMemoCache(2);
|
||||
let t0;
|
||||
if ($[0] !== props.bar) {
|
||||
|
||||
@@ -65,8 +65,14 @@ function makePluginOptions(
|
||||
}
|
||||
if (firstLine.includes("@instrumentForget")) {
|
||||
enableEmitInstrumentForget = {
|
||||
source: "react-forget-runtime",
|
||||
importSpecifierName: "useRenderCounter",
|
||||
fn: {
|
||||
source: "react-forget-runtime",
|
||||
importSpecifierName: "useRenderCounter",
|
||||
},
|
||||
gating: {
|
||||
source: "react-forget-runtime",
|
||||
importSpecifierName: "shouldInstrument",
|
||||
},
|
||||
};
|
||||
}
|
||||
if (firstLine.includes("@enableEmitFreeze")) {
|
||||
@@ -282,6 +288,9 @@ export function transformFixtureInput(
|
||||
const filename =
|
||||
path.basename(fixturePath) + (language === "typescript" ? ".ts" : "");
|
||||
const inputAst = parseInput(input, filename, language);
|
||||
// Give babel transforms an absolute path as relative paths get prefixed
|
||||
// with `cwd`, which is different across machines
|
||||
const virtualFilepath = "/" + filename;
|
||||
|
||||
const presets =
|
||||
language === "typescript"
|
||||
@@ -292,7 +301,7 @@ export function transformFixtureInput(
|
||||
* Get Forget compiled code
|
||||
*/
|
||||
const forgetResult = transformFromAstSync(inputAst, input, {
|
||||
filename,
|
||||
filename: virtualFilepath,
|
||||
highlightCode: false,
|
||||
retainLines: true,
|
||||
plugins: [
|
||||
@@ -324,7 +333,7 @@ export function transformFixtureInput(
|
||||
);
|
||||
const result = transformFromAstSync(forgetResult.ast, forgetOutput, {
|
||||
presets,
|
||||
filename,
|
||||
filename: virtualFilepath,
|
||||
});
|
||||
if (result?.code == null) {
|
||||
return {
|
||||
@@ -348,7 +357,7 @@ export function transformFixtureInput(
|
||||
try {
|
||||
const result = transformFromAstSync(inputAst, input, {
|
||||
presets,
|
||||
filename,
|
||||
filename: virtualFilepath,
|
||||
});
|
||||
|
||||
if (result?.code == null) {
|
||||
|
||||
Reference in New Issue
Block a user