[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:
Mofei Zhang
2024-02-27 22:06:36 -08:00
parent 5acfc13575
commit a06ded902a
10 changed files with 90 additions and 37 deletions
@@ -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.
@@ -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 ?? "")]
)
)
);
@@ -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) {
@@ -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) {
@@ -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) {
+14 -5
View File
@@ -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) {