Renders, effects, and profiling
Measure React work, render causes, effect schedules, errors, and before/after changes.
The JSON examples below show selected fields from the full response.
react_clear_renders
Clears render counters and starts a new causal observation window.
Input: components defaults to [], accepts at most 50 display-name substrings, and trims each
1–160 character string. roots defaults to [] and accepts at most 20 integer component IDs.
budget is optional. fiberLimit defaults to 250, operationLimit to 20000, timeLimitMs to 8,
targetOperationReserve to 4000, targetTimeReserveMs to 4, and adaptive to true; the numeric ranges are
50–20000, 1000–2000000, 1–500, 100–500000, and 0.5–250 respectively. lifecycle
defaults to {bufferLimit:1000,targetReserve:100}; bufferLimit is an integer from 100 to 20000,
targetReserve is an integer from 0 to 5000, and the reserve cannot exceed the buffer.
The expanded maxima are opt-in; defaults are unchanged. adaptive:true increases only later commit
budgets after exhaustion and cannot restore evidence skipped by the exhausted commit. For a
one-action window, especially on a large React Native tree, discard incomplete coverage and rerun
the action from a fresh window with an explicitly larger budget.
npx @genie-react/cli call react_clear_renders \
'{"components":["ProductCard"],"budget":{"fiberLimit":500,"timeLimitMs":12}}' --jsonOutput (selected fields):
{
"ok": true,
"tracking": true,
"documentCommitId": 18,
"observation": {
"id": "observation:2",
"epoch": 2,
"startedAfterDocumentCommitId": 18
},
"observationConfig": {
"adaptive": true,
"fiberLimit": 500,
"timeLimitMs": 12
}
}Agent use: Drive exactly one interaction next, then join render and effect evidence by
observation.id. This call discards earlier counters and starts fresh retained histories, so capture
needed evidence before clearing again. Use the subsequent render and cause evidence only when its
coverage is complete and budgetExhaustedCommits is zero; adaptive recovery later in the window
does not repair an earlier incomplete commit.
react_get_renders
Summarizes component render counts, changed inputs, timing, mount identity, and source.
Input: component is an optional component-name substring. sort is renders, updates, unnecessary,
referenceOnly, unstable, or selfTime and defaults to renders. limit is an integer from 1 to
200 and defaults to 40. appOnly defaults to true. nameFilter accepts a case-insensitive
whole-name glob (1–160 characters): * matches any text and ? matches one character. excludeNames
accepts up to 50 such globs. minUpdates is a nonnegative integer and defaults to 0. These selectors
combine with the existing component substring and run before source classification and the cap.
Use nextCursor with cursor to read the remainder; pass only cursor and optional limit on
continuation calls. Pages retain the first report's ordering, counts, summary, source coverage, and
observation metadata while new commits arrive. pagination reports the snapshot ID, row offset,
total selected rows, and expiresAt in Unix milliseconds. omittedByLimit counts rows remaining
after the current page, and nextCursor:null means that selected snapshot is exhausted.
Cursors expire after five minutes or eviction by newer reports (at most three paginated snapshots
are retained). Clearing/restarting profiling or disposing tracking invalidates them. Reports with
more than 5000 candidates matching the name/update filters (before appOnly) fail before snapshot
construction, with a narrowing instruction. Invalid or expired cursors require a fresh call without
cursor.
includeCursor defaults to true. Set it to false for a one-shot report: the requested row limit
still applies, the summary covers all selected records, and omitted rows are not recoverable by
cursor. No snapshot is retained, pagination.snapshotId and expiresAt are null, and the 5000-candidate
snapshot limit does not apply. Background React quiet waits use this mode so polling cannot evict
an active paginated report.
Source ownership is separate from pagination. If sourceClassification.complete is false,
appOnly:true excludes unknown ownership and the result remains a lower bound even after its last
page. Retry without cursor after source warmup to include newly classified rows.
Both react_get_renders and react_profile_report expose bundle (development, production,
mixed, or unknown), timingsBundleDependent: true, and countsScope: "observed-run".
The bundle describes registered React renderers in the app document. Timing values depend on that
bundle and instrumentation. Counts describe this measured run, subject to coverage; they are not
production estimates. Development Strict Mode behavior can differ from production. Compare runs
with equivalent conditions and check collection availability even when the bundle is production.
sourceClassification reports app, library, and unknown ownership counts for matching records
before appOnly and the result limit. totalCandidates counts that set; evaluated counts
completed source lookups (including unresolved results and cache hits). complete is true only
when every candidate has app or library evidence. Unknown ownership never counts as library.
Cold documents can exceed the source lookup budget. Genie warms the remaining sources in the
background; retry the report to check recovery. If appOnly:true excludes unknown ownership,
comparable is false with app-source-classification-incomplete, and summary semantics are
lower-bound or unknown. appOnly:false keeps those records visible without inventing their
ownership. If unknowns persist after relaunch, inspect this unfiltered report and restore source
metadata or source-map access before comparing app-only measurements.
For const FriendListRow = memo(() => ...), the wrapper does not retain the variable's name.
Genie uses an explicit wrapper displayName, then the inner function's name, and finally source
information when available. To preserve anonymous memo binding names automatically, add the
opt-in genie-react/babel plugin to your development Babel pipeline.
For Expo, add it to your existing Babel configuration (keep your other presets and plugins):
module.exports = function (api) {
api.cache(true)
return {
presets: ['babel-preset-expo'],
plugins: ['genie-react/babel'],
}
}For Vite 8 with @vitejs/plugin-react 6, install @rolldown/plugin-babel and @babel/core@^7
and add the naming plugin through that Babel adapter:
import babel from '@rolldown/plugin-babel'
import react from '@vitejs/plugin-react'
import genieNames from 'genie-react/babel'
import { genie } from 'genie-react/vite'
import { defineConfig } from 'vite'
export default defineConfig(({ command }) => ({
plugins: [genie(), react(), command === 'serve' && babel({ plugins: [genieNames] })],
}))Older @vitejs/plugin-react versions that expose babel.plugins can register
'genie-react/babel' there instead.
Restart Metro with --clear or restart Vite after changing Babel configuration. The plugin
supports Babel 7 and only annotates inline anonymous arrows or function expressions passed to
an import-proven React memo call, including import aliases and React.memo. It preserves
named inner functions and explicit display names. Nested wrapper calls, assignment expressions,
and components without a named variable declaration keep the existing runtime fallback.
Babel production/test environments and Metro release callers receive no added metadata.
The plugin changes debug names only; it does not enable render collection in release builds.
npx @genie-react/cli call react_get_renders \
'{"component":"ProductCard","sort":"selfTime","limit":10,"appOnly":true}' --jsonTo select updated rows and exclude an internal wrapper before paging:
npx @genie-react/cli call react_get_renders \
'{"nameFilter":"*Row*","excludeNames":["*Internal*"],"minUpdates":1,"limit":200}' --jsonPass the returned cursor to continue:
npx @genie-react/cli call react_get_renders \
'{"cursor":"01234567-89ab-4cde-8fab-0123456789ab:200","limit":200}' --jsonOutput (selected fields):
{
"tracking": true,
"commits": 3,
"documentCommitId": 21,
"observation": {
"id": "observation:2",
"epoch": 2,
"startedAfterDocumentCommitId": 18
},
"summary": { "totalRenders": 12, "totalUpdates": 8 },
"components": [
{
"id": 51,
"name": "ProductCard",
"instance": {
"mountId": "mount:9",
"key": "sku-1",
"logicalPath": "ProductList[key=sku-1] > ProductCard[key=sku-1]",
"logicalIdentityEvidence": "keyed",
"mountGeneration": 1
},
"renders": 4,
"updates": 4,
"selfTime": 1.2,
"cumulativeSelfTime": 3.4,
"causes": [
{
"kind": "state",
"evidence": "exact",
"name": "state[0]",
"before": { "selected": false },
"after": { "selected": true },
"deepDiff": {
"changes": [
{
"kind": "value",
"path": "selected",
"before": false,
"after": true
}
],
"visited": 2,
"truncated": false
},
"hook": { "index": 0, "stateIndex": 0, "kind": "state" }
}
]
}
],
"omittedByLimit": 0,
"coverage": { "complete": true, "inputAttributionComplete": true }
}Agent use: Optimize the highest cumulative cost only after the exact changed path and complete
coverage explain the render. Compatibility counts such as unnecessary are not proof that a render
is safe to remove.
react_render_causes
Returns joinable render events and their observed causes.
For a single identified Query observer, Genie omits an inferred cause when its complete current
notification policy excludes every changed subscribed field. notificationPolicyCheck.status
is matched with changedSubscribedFields when a subscribed field changed, or unavailable
with a reason when the policy, identity, or field comparison cannot be checked. The basis is
current-effective-policy: this checks compatibility with current options, not historical
notification delivery. Dynamic and private auto-tracked policies remain unavailable. Implicit
error subscriptions from throwOnError are included; recorded exact deliveries take precedence
and are never removed by this check.
Input: commit and afterCommit are optional nonnegative integers and are mutually exclusive.
component is an optional substring. limit is an integer from 1 to 500 and defaults to 100.
appOnly defaults to true.
npx @genie-react/cli call react_render_causes \
'{"component":"ProductCard","afterCommit":1,"limit":20,"appOnly":true}' --jsonOutput (selected fields):
{
"tracking": true,
"documentCommitId": 21,
"observation": {
"id": "observation:2",
"epoch": 2,
"startedAfterDocumentCommitId": 18
},
"events": [
{
"renderEventId": "render:20:7",
"observationId": "observation:2",
"commitId": 2,
"documentCommitId": 20,
"componentId": 51,
"componentName": "ProductCard",
"instance": {
"mountId": "mount:9",
"key": "sku-1",
"mountGeneration": 1,
"logicalIdentityEvidence": "keyed"
},
"causes": [
{
"kind": "state",
"evidence": "exact",
"name": "state[0]",
"before": false,
"after": true,
"deepDiff": {
"changes": [
{ "kind": "value", "path": "", "before": false, "after": true }
],
"visited": 1,
"truncated": false
},
"hook": { "index": 0, "stateIndex": 0, "kind": "state" }
}
],
"sourceOwnership": "app"
}
],
"omittedByLimit": 0,
"coverage": {
"complete": true,
"inputAttributionComplete": true,
"droppedRenderEvents": 0
},
"renderEventRetention": {
"evictedEvents": 0,
"earliestDocumentCommitId": 19,
"latestDocumentCommitId": 20
}
}Agent use: Join renderEventId, observation, document commit, mount, and hook IDs to the action,
then change the producer of the exact input. Repeat the interaction if events were evicted or
coverage is incomplete.
react_component_cohort
Reports lifecycle outcomes for every matching component instance in the current observation.
Input: component is a required non-empty string. exact defaults to true. limit is an
integer from 1 to 200 and defaults to 50.
appOnly defaults to false; set it to true to include only proven app-owned instances.
Ownership includes mounted instances and cached unmount provenance. Unknown ownership is excluded
by the app filter and lowers coverage. documentCommitId marks the captured traversal;
attribution.status: "stale" means commits or a clear raced source lookup.
Each instance also reports its current renderingState, independently of recorded updates:
mounted-rendering: mounted and eligible to render; it may have stayed idle during the observation.mounted-frozen: retained under a registeredreact-freezeboundary whose primary subtree has committed suspension.mounted-hidden: retained under React Offscreen hiding, without provenreact-freezeorigin (for example, Activity).mounted-unknown: the boundary state is unavailable.unmounted: an actual unmount was observed during this observation.
Enable freeze identification in your development entry, alongside the Genie hook:
import 'genie-react/react-freeze'The optional adapter imports and registers the actual Freeze export from react-freeze 1.x.
It also identifies the same export used internally by react-native-screens when
enableFreeze(true) suspends a screen. No component-name heuristic is used. If your workspace
resolves multiple copies, register the app's public import too:
import { registerReactFreeze } from 'genie-react/react-freeze'
import { Freeze } from 'react-freeze'
registerReactFreeze(Freeze)Use the same react-freeze package version/resolution as your navigation dependency. A copy
that was not registered remains generic hidden evidence; the adapter never guesses its identity.
coverage.freezeDetection reports whether the adapter is enabled. Frozen status requires
committed suspension, so a delayed or pending freeze request does not count yet. Suspense
fallback components remain eligible to render. Thawing preserves mount identity; actual
unmounting produces a tombstone. None of these states proves effect or Query observer subscriptions.
Each instance reports reactVisibility independently of its lifecycle status:
hidden: a committed React Offscreen ancestor has hidden state, including Activity or suspended content.not-hidden: no hidden React Offscreen ancestor was observed; this does not prove CSS or native screen visibility.unknown: visibility cannot be established, including unmounted tombstones or incomplete boundary state.
A hidden instance remains in the mounted cohort with its mount identity. Revealing it preserves
that identity; an observed full unmount produces an unmounted entry. React visibility does not
prove which navigation or freeze mechanism hid the subtree. Activity can clean up effects and
subscriptions while preserving state, so disappearing observers alone do not establish a full
unmount. react-freeze uses Suspense, and generic hidden evidence cannot establish its origin.
npx @genie-react/cli call react_component_cohort \
'{"component":"ProductCard","exact":true,"limit":100}' --jsonOutput (selected fields):
{
"observation": {
"id": "observation:2",
"epoch": 2,
"startedAfterDocumentCommitId": 18
},
"query": { "component": "ProductCard", "exact": true },
"status": "updated",
"matched": 1,
"mountedUpdated": 1,
"mountedIdle": 0,
"mountedUnknown": 0,
"unmounted": 0,
"returned": 1,
"omittedByLimit": 0,
"instances": [
{
"componentName": "ProductCard",
"status": "mounted-updated",
"instance": {
"mountId": "mount:9",
"key": "sku-1",
"logicalIdentityEvidence": "keyed",
"mountGeneration": 1
},
"profileCommitId": 2,
"documentCommitId": 20
}
],
"coverage": { "complete": true, "scanTruncated": false }
}Agent use: Decide whether a list fix targets updated rows, unmounted rows, or identity churn. Read
each returned instance's status even when global coverage.complete is false: mounted-updated and
mounted-idle remain authoritative, as does an exact unmounted tombstone; mounted-unknown does
not. The tool emits top-level absent only when root and lifecycle evidence is sufficient. Do not
manufacture absence from matched: 0 when the status is unknown, or from omitted instances.
react_effect_audit
Ranks effect schedules and reports dependency, cleanup, hotness, and ownership evidence.
Input: component is an optional substring. onlyHot defaults to false. appOnly defaults to
true. packageName is an optional trimmed string of 1–214 characters and requires
appOnly:false. minUpdates is an integer from 1 to 1000 and defaults to 3. Legacy minFireRate
is from 0.1 to 1 and defaults to 1. Preferred minScheduleRate is optional, is from 0.1 to 1, and
takes precedence. limit is an integer from 1 to 200 and defaults to 40.
npx @genie-react/cli call react_effect_audit \
'{"component":"Search","onlyHot":true,"minUpdates":5,"minScheduleRate":0.8,"limit":20}' --jsonOutput (selected fields):
{
"tracking": true,
"commits": 6,
"documentCommitId": 24,
"attribution": { "status": "current" },
"hotnessCriteria": {
"minUpdates": 5,
"minScheduleRate": 0.8,
"minFireRate": 0.8,
"confidenceLevel": 0.95
},
"components": [
{
"id": 60,
"name": "Search",
"componentProvenance": {
"ownership": "app",
"evidence": "inferred",
"reason": "nearest-symbolicated-fiber",
"source": {
"file": "/src/Search.tsx",
"line": 12,
"column": 1,
"functionName": "Search"
}
},
"effects": [
{
"index": 1,
"kind": "effect",
"depsMode": "list",
"depCount": 1,
"scheduled": 5,
"updates": 5,
"lastChangedDep": 0,
"cleanupFunctionObserved": true,
"provenance": {
"ownership": "app",
"evidence": "exact",
"reason": "exact-hook-order",
"hookCallsite": {
"file": "/src/useSearch.ts",
"line": 21,
"column": 3,
"functionName": "useSearch"
},
"hookDefinitionOwner": {
"file": "/src/useSearch.ts",
"line": 8,
"column": 1,
"functionName": "useSearch"
},
"package": null,
"sourceMapConfidence": "mapped",
"failureReason": null
},
"hotness": {
"label": "hot",
"samples": 5,
"observedRate": 1,
"minUpdates": 5,
"minScheduleRate": 0.8,
"confidenceInterval": { "level": 0.95, "lower": 0.57, "upper": 1 },
"scheduleReason": "meets-threshold"
}
}
]
}
],
"omittedByLimit": 0,
"effectsOmittedByLimit": 0,
"coverage": { "complete": true, "inputAttributionComplete": true }
}Agent use: Inspect the exact hook callsite and changed dependency before editing the effect. A schedule does not prove execution or cleanup timing, and unknown ownership must not be assigned to the app.
react_effect_timeline
Returns the preferred causal timeline for effect scheduling, execution, cleanup, and observed consequences.
Input: component is an optional substring. afterDocumentCommitId is an optional nonnegative
integer. limit is an integer from 1 to 500 and defaults to 100.
appOnly defaults to false. Set it to true to filter by the component that owns the scheduled
effect. This ownership differs from the hook source reported by react_effect_audit.
Read ownershipCoverage even when filtering leaves no events.
npx @genie-react/cli call react_effect_timeline \
'{"component":"Search","afterDocumentCommitId":18,"limit":20}' --jsonOutput (selected fields):
{
"tracking": false,
"documentCommitId": 21,
"observation": {
"id": "observation:2",
"epoch": 2,
"startedAfterDocumentCommitId": 18
},
"events": [
{
"effectEventId": "effect:20:8",
"effectId": "effect:mount:4:1",
"observationId": "observation:2",
"commitId": 2,
"documentCommitId": 20,
"componentId": 60,
"componentName": "Search",
"mountId": "mount:4",
"effectIndex": 1,
"kind": "effect",
"phase": "update",
"event": "scheduled",
"evidence": "exact",
"changedDependencySlots": [0],
"changedDependencySlotsOmitted": 0,
"dependencySlotsUnscanned": 0,
"execution": {
"status": "observed",
"evidence": "exact",
"startedAt": 1042.1,
"completedAt": 1043.34,
"durationMs": 1.24,
"outcome": "completed"
},
"cleanupExecution": {
"status": "unobserved",
"reason": "no-cleanup-returned"
},
"consequences": {
"status": "instrumented",
"observedDomains": [
"query-notification",
"router-notification",
"react-commit"
],
"unobservedDomains": [
"state-update",
"external-store-write",
"network",
"event-listener",
"navigation"
],
"events": [
{
"kind": "notification",
"domain": "query-notification",
"notificationId": "query-notification:7",
"timestamp": 1043.5,
"evidence": "exact"
},
{
"kind": "resulting-commit",
"documentCommitId": 21,
"timestamp": 1044.2,
"evidence": "inferred",
"reason": "next-commit-after-effect-execution"
}
]
},
"timeline": [
{
"stage": "schedule",
"timestamp": 1041.8,
"evidence": "exact",
"referenceId": "effect:20:8"
},
{
"stage": "execution",
"timestamp": 1042.1,
"evidence": "exact",
"outcome": "completed"
},
{
"stage": "consequence",
"timestamp": 1043.5,
"evidence": "exact",
"referenceId": "query-notification:7"
},
{
"stage": "resulting-commit",
"timestamp": 1044.2,
"evidence": "inferred"
}
]
}
],
"omittedByLimit": 0,
"evictedEvents": 0,
"droppedEvents": 0,
"coverage": { "complete": true }
}Agent use: Follow the exact notification ID to its consumer, but treat the next React commit as inferred and the listed unobserved domains as unknown. Repeat a clean observation when events were evicted or coverage is incomplete.
react_effect_events
Returns the same bounded effect causal timeline under the legacy event-oriented name.
Input: component is an optional substring. afterDocumentCommitId is an optional nonnegative
integer. limit is an integer from 1 to 500 and defaults to 100.
appOnly defaults to false and filters by the component that owns the scheduled effect.
Read ownershipCoverage when interpreting an empty or partial result.
npx @genie-react/cli call react_effect_events \
'{"component":"Search","afterDocumentCommitId":18,"limit":20}' --jsonOutput (selected fields):
{
"tracking": true,
"documentCommitId": 20,
"events": [
{
"effectEventId": "effect:20:8",
"effectId": "effect:mount:4:1",
"observationId": "observation:2",
"commitId": 2,
"documentCommitId": 20,
"componentName": "Search",
"mountId": "mount:4",
"effectIndex": 1,
"kind": "effect",
"phase": "update",
"event": "scheduled",
"evidence": "exact",
"changedDependencySlots": [0],
"changedDependencySlotsOmitted": 0,
"dependencySlotsUnscanned": 0,
"execution": { "status": "unobserved", "reason": "not-yet-observed" },
"cleanupExecution": {
"status": "unobserved",
"reason": "not-yet-observed"
},
"consequences": {
"status": "instrumented",
"observedDomains": [
"query-notification",
"router-notification",
"react-commit"
],
"unobservedDomains": [
"state-update",
"external-store-write",
"network",
"event-listener",
"navigation"
],
"events": []
},
"timeline": [
{
"stage": "schedule",
"timestamp": 1041.8,
"evidence": "exact",
"referenceId": "effect:20:8"
}
]
}
],
"omittedByLimit": 0,
"evictedEvents": 0,
"droppedEvents": 0,
"coverage": { "complete": true }
}Agent use: Wait and read again when execution is not-yet-observed; do not infer that the
effect ran from its schedule. Prefer react_effect_timeline in new workflows.
react_error_state
Reads caught errors, active Suspense boundaries, and a blank-tree hint.
Input: includeSource defaults to true. limit is an integer from 1 to 100 and defaults to 20.
appOnly defaults to false. With true, ownership is resolved even when includeSource is
false; that option omits source payloads while preserving ownership filtering.
npx @genie-react/cli call react_error_state \
'{"includeSource":true,"limit":20}' --jsonOutput (selected fields):
{
"caughtErrors": [
{
"boundaryId": 10,
"boundaryName": "CheckoutErrorBoundary",
"boundarySource": {
"file": "/src/CheckoutErrorBoundary.tsx",
"line": 7,
"column": 1,
"functionName": "CheckoutErrorBoundary"
},
"throwingComponent": "PaymentForm",
"message": "Payment failed",
"isLibraryBoundary": false,
"forced": false
}
],
"suspended": [],
"blankTreeHint": null
}Agent use: Inspect the error, stack, throwing component, and boundary, then fix only the proven
producer. If forced:true, reset overrides before diagnosing production behavior.
react_refresh_events
Reports Fast Refresh files, preserved instances, remounts, and excluded profile commits.
Input: afterSequence is an optional nonnegative integer. limit is an integer from 1 to 50
and defaults to 10. includeSource defaults to true.
appOnly defaults to false. With true, ownership is resolved even when includeSource is
false. Counters remain raw event totals; filtered component names require retained app ownership.
npx @genie-react/cli call react_refresh_events \
'{"afterSequence":12,"limit":10,"includeSource":true}' --jsonOutput (selected fields):
{
"events": [
{
"sequence": 13,
"timestamp": 1752746705000,
"filePaths": ["/src/Cart.tsx"],
"updatedComponents": ["Cart"],
"remountedComponents": [],
"preservedState": [
{
"id": 31,
"name": "Cart",
"source": {
"file": "/src/Cart.tsx",
"line": 9,
"column": 1,
"functionName": "Cart"
},
"isLibrary": false
}
],
"counts": {
"updatedComponents": 1,
"remountedComponents": 0,
"updatedFibers": 1,
"remountedFibers": 0
},
"profileCommitsExcluded": 1,
"truncated": false
}
],
"latestSequence": 13,
"droppedEvents": 0,
"partialSources": false
}Agent use: Rerun the measured interaction when a refresh overlaps it, and investigate component identity when the edit remounted state that should have been preserved.
react_profile_start
Clears render counters and starts a profiling observation.
Input: components defaults to [], accepts at most 50 display-name substrings, and trims each
1–160 character string. roots defaults to [] and accepts at most 20 integer component IDs.
budget is optional. fiberLimit defaults to 250, operationLimit to 20000, timeLimitMs to 8,
targetOperationReserve to 4000, targetTimeReserveMs to 4, and adaptive to true; the numeric ranges are
50–20000, 1000–2000000, 1–500, 100–500000, and 0.5–250 respectively. lifecycle
defaults to {bufferLimit:1000,targetReserve:100}; bufferLimit is an integer from 100 to 20000,
targetReserve is an integer from 0 to 5000, and the reserve cannot exceed the buffer.
The expanded maxima are opt-in; defaults are unchanged. adaptive:true increases only later commit
budgets after exhaustion and cannot restore evidence skipped by the exhausted commit. For a
one-action profile, especially on a large React Native tree, discard incomplete coverage and rerun
the flow from a fresh profile with an explicitly larger budget.
npx @genie-react/cli call react_profile_start \
'{"components":["SearchResults"],"lifecycle":{"bufferLimit":2000,"targetReserve":200}}' --jsonOutput (selected fields):
{
"ok": true,
"tracking": true,
"documentCommitId": 24,
"observation": {
"id": "observation:3",
"epoch": 3,
"startedAfterDocumentCommitId": 24
},
"observationConfig": {
"lifecycleBufferLimit": 2000,
"lifecycleTargetReserve": 200
}
}Agent use: Run the exact flow to measure next, then stop before reading the final report. Starting
clears current counters but keeps named snapshots for later diffs. Compare the result only when
coverage is complete and budgetExhaustedCommits is zero.
react_profile_stop
Stops profiling while retaining aggregates and named snapshots.
Input: {}; this tool has no input fields.
npx @genie-react/cli call react_profile_stop '{}' --jsonOutput (selected fields):
{ "ok": true, "tracking": false, "commits": 5 }Agent use: Read reports from the frozen five-commit window before another start or clear changes the counters. Stopping does not uninstall instrumentation or delete retained snapshots.
react_profile_snapshot
Stores current app-component aggregates as a named diff baseline.
Input: label is a string and defaults to baseline; reusing a label replaces its snapshot.
appOnly defaults to true. Use the same value for the later react_renders_diff call.
If app filtering excludes unknown ownership, the baseline remains a lower bound; warming source
metadata later cannot repair this stored baseline.
npx @genie-react/cli call react_profile_snapshot \
'{"label":"search-before"}' --jsonOutput (selected fields):
{
"ok": true,
"label": "search-before",
"commits": 5,
"components": 18,
"coverage": { "complete": true, "inputAttributionComplete": true }
}Agent use: Keep this baseline only if coverage is complete, then run the same flow after the change and compare it by label. Reuse the label to replace stale retained data.
react_renders_diff
Compares current render aggregates with a named profile snapshot.
Input: baseline is a string and defaults to baseline. thresholdMs is a number with no
contract range and defaults to 0.5.
appOnly defaults to true and must match the stored baseline. Read both sides' coverage,
including sourceClassification, before comparing totals.
npx @genie-react/cli call react_renders_diff \
'{"baseline":"search-before","thresholdMs":0.5}' --jsonOutput (selected fields):
{
"baseline": "search-before",
"commits": { "before": 5, "after": 4 },
"clearsSinceBaseline": 1,
"coverage": {
"baseline": { "complete": true },
"current": { "complete": true }
},
"selfTimeMs": { "before": 14.2, "after": 8.1, "delta": -6.1, "pct": -43 },
"regressed": [],
"improved": [
{
"name": "SearchResults",
"source": "/src/SearchResults.tsx:11",
"deltaMs": -5.4,
"before": { "renders": 5, "selfTime": 11.7 },
"after": { "renders": 4, "selfTime": 6.3 }
}
],
"added": [],
"removed": []
}Agent use: Accept the change only when equivalent flows and complete coverage show the intended
component improved without regressions. When counters were cleared, removed means not observed in
the current window, not deleted from source.
react_profile_report
Returns bounded render-cost and render-count leaderboards for the current profile.
Input: limit is an integer from 1 to 100 and defaults to 20.
component is an optional display-name substring and filters before the leaderboard limit.
appOnly defaults to false; set it to true for proven app-owned components. Unknown ownership
excluded by that filter lowers coverage.
npx @genie-react/cli call react_profile_report \
'{"limit":10}' --jsonOutput (selected fields):
{
"commits": 4,
"tracking": false,
"documentCommitId": 28,
"attribution": { "status": "current" },
"coverage": { "complete": true, "inputAttributionComplete": true },
"slowest": [
{ "id": 51, "name": "ProductCard", "selfTime": 2.1, "renders": 4 }
],
"mostRerendered": [
{ "id": 51, "name": "ProductCard", "renders": 4, "unnecessary": 0 }
],
"mostReferenceOnly": []
}Agent use: Use the slowest or most-rendered row as a shortlist, then inspect its exact render causes before editing. Causal leaderboards are not sufficient when input attribution is incomplete.
react_measure
Opens a labelled measurement without clearing global counters.
Input: label is required, trimmed, and
1–160 characters. The response includes handle, label, startedAfterDocumentCommitId, and
expiresAt (Unix milliseconds). Collection must be active and able to observe commits.
npx @genie-react/cli call react_measure '{"label":"open checkout"}' --jsonOutput (selected fields):
{"handle":"span-id","label":"open checkout","startedAfterDocumentCommitId":18,"ownership":"newest-open-span","attribution":"temporal-only"}Agent use: Drive the UI next, then read and close the returned handle.
Ownership is explicitly newest-open-span: while multiple spans are open, only the newest records
commits. Closing it resumes the previous span. This gives disjoint commit sets, not causal isolation;
background work can still be included. At most 20 spans are retained for five minutes. A closed span
may be evicted at capacity; opening fails if all 20 are open. Reload/Fast Refresh invalidates handles.
react_renders_since
Reads the labelled span's owned commits.
Input: handle is required (1–100 characters), close defaults to
false, component optionally filters by name substring, appOnly defaults to true, and limit
is an integer from 1 to 200 (default 40). Set close:true after driving the interaction and waiting
for commits to settle. Re-reading a closed span preserves its captured counts.
npx @genie-react/cli call react_renders_since '{"handle":"<returned handle>","close":true}' --jsonOutput (selected fields):
{"label":"open checkout","commitIds":[19,21],"commits":2,"excludedCommits":1,"coverage":{"complete":true,"semantics":"exact"}}Agent use: Compare labelled counts without clearing another span's evidence.
The response carries label, disjoint commitIds, excludedCommits owned by newer spans,
components, summary, sourceClassification, and coverage. Counts are frozen before source
lookup. omittedByLimit identifies rows outside the selected page; the summary covers all selected
components. Global clear does not erase span evidence. A span retains at most 500 components and
1000 commits; hitting the commit cap closes it. Collection and retention losses make coverage
incomplete. Unresolved source ownership also makes an app-only report incomplete; use
appOnly:false to inspect all retained records. attribution:"temporal-only" explicitly means
these counts do not prove which interaction caused a commit.
react_quiesce
Waits until React document commits have remained unchanged for the requested interval.
Input: idleMs is an integer from 100 to 5000 (default 500); timeoutMs is an integer from
1 to 60000 (default 10000). Unknown fields are rejected. The CLI transport allows the requested deadline.
npx @genie-react/cli call react_quiesce '{"idleMs":500,"timeoutMs":10000}' --jsonOutput (selected fields):
{"ok":true,"outcome":"idle","elapsedMs":750,"observedCommits":3,"documentCommitId":21,"renderCollection":"available"}Agent use: Clear, drive one UI interaction, quiesce, then read the render report. Outcomes are
idle, timed-out, or unavailable; observedCommits counts increases between valid samples,
excluding commits before the first sample. A missing collector, changed document, or counter reset
cannot prove idle. Historical late-hook degradation remains visible but can establish a new quiet
interval. Failed samples restart the quiet interval; silent requests respect the overall deadline.
Tool-catalog refreshes in the same document preserve the wait. Add --fail-on-result-error for a
nonzero exit on a result with ok:false. This observes React commits, not continuous canvas/native
frames, query freshness, or future scheduled work.