Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion packages/skia/cpp/api/recorder/JsiRecorder.h
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,16 @@ class JsiRecorder : public JsiSkWrappingSharedPtrHostObject<Recorder> {
return jsi::Value::undefined();
}

JSI_HOST_FUNCTION(visit) {
auto root = arguments[0].asObject(runtime).asArray(runtime);
auto sharedValues = getObject()->visit(runtime, root);
return std::move(sharedValues);
}

JSI_HOST_FUNCTION(getVariableCount) {
return jsi::Value(static_cast<int>(getObject()->getVariableCount()));
}

EXPORT_JSI_API_TYPENAME(JsiRecorder, Recorder)

JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiRecorder, saveGroup),
Expand Down Expand Up @@ -346,7 +356,9 @@ class JsiRecorder : public JsiSkWrappingSharedPtrHostObject<Recorder> {
JSI_EXPORT_FUNC(JsiRecorder, drawSkottie),
JSI_EXPORT_FUNC(JsiRecorder, play),
JSI_EXPORT_FUNC(JsiRecorder, applyUpdates),
JSI_EXPORT_FUNC(JsiRecorder, reset))
JSI_EXPORT_FUNC(JsiRecorder, reset),
JSI_EXPORT_FUNC(JsiRecorder, visit),
JSI_EXPORT_FUNC(JsiRecorder, getVariableCount))

// This has no basis in reality but since since these are private long-lived
// objects, we think it is more than fine.
Expand Down
478 changes: 478 additions & 0 deletions packages/skia/cpp/api/recorder/RNRecorder.h

Large diffs are not rendered by default.

258 changes: 258 additions & 0 deletions packages/skia/src/renderer/__tests__/e2e/VisitPerformance.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import React from "react";

import { Group, Rect, Circle, Fill } from "../../components";
import { surface, mountCanvas } from "../setup";
import { visit } from "../../../sksg/Recorder/Visitor";
import { Recorder } from "../../../sksg/Recorder/Recorder";

// Helper to create a deeply nested tree structure
const DeepTree = ({
depth,
breadth,
currentDepth = 0,
}: {
depth: number;
breadth: number;
currentDepth?: number;
}): React.ReactElement => {
if (currentDepth >= depth) {
// Leaf nodes - simple shapes
return (
<Group>
<Rect x={currentDepth} y={currentDepth} width={10} height={10} />
<Circle cx={currentDepth + 5} cy={currentDepth + 5} r={5} />
</Group>
);
}

// Create multiple children at each level
const children: React.ReactElement[] = [];
for (let i = 0; i < breadth; i++) {
children.push(
<Group
key={i}
transform={[{ translateX: i * 10 }, { translateY: currentDepth * 10 }]}
>
<DeepTree depth={depth} breadth={breadth} currentDepth={currentDepth + 1} />
</Group>
);
}

return (
<Group opacity={0.9}>
{children}
</Group>
);
};

// Helper to create a wide tree with many siblings
const WideTree = ({ count }: { count: number }): React.ReactElement => {
const children: React.ReactElement[] = [];
for (let i = 0; i < count; i++) {
children.push(
<Group key={i} transform={[{ translateX: i % 100, translateY: Math.floor(i / 100) }]}>
<Rect x={0} y={0} width={5} height={5} color="red" />
<Circle cx={2.5} cy={2.5} r={2} color="blue" />
</Group>
);
}
return <Group>{children}</Group>;
};

// Helper to create a complex tree with mixed content
const ComplexTree = ({
groups,
shapesPerGroup,
}: {
groups: number;
shapesPerGroup: number;
}): React.ReactElement => {
const groupElements: React.ReactElement[] = [];
for (let g = 0; g < groups; g++) {
const shapes: React.ReactElement[] = [];
for (let s = 0; s < shapesPerGroup; s++) {
const x = (g * shapesPerGroup + s) % 256;
const y = Math.floor((g * shapesPerGroup + s) / 256);
shapes.push(
<Group key={s} opacity={0.8}>
<Rect x={x} y={y} width={4} height={4} color="green" />
</Group>
);
}
groupElements.push(
<Group
key={g}
transform={[{ rotate: g * 0.01 }]}
origin={{ x: 128, y: 128 }}
>
{shapes}
</Group>
);
}
return (
<Group>
<Fill color="white" />
{groupElements}
</Group>
);
};

// Benchmark results collector
interface BenchmarkResult {
name: string;
nodes: string;
iterations: number;
avg: number;
min: number;
max: number;
}

const results: BenchmarkResult[] = [];

const runBenchmark = (
name: string,
nodes: string,
iterations: number,
fn: () => void
): BenchmarkResult => {
const times: number[] = [];

for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
const end = performance.now();
times.push(end - start);
}

const avg = times.reduce((a, b) => a + b, 0) / times.length;
const min = Math.min(...times);
const max = Math.max(...times);

return { name, nodes, iterations, avg, min, max };
};

const printResultsTable = () => {
// eslint-disable-next-line no-console
console.log("\n┌─────────────────────────────────────────────────────────────────────────────────────┐");
// eslint-disable-next-line no-console
console.log("│ Visit Performance Benchmark │");
// eslint-disable-next-line no-console
console.log("├──────────────────────────────┬──────────┬──────┬───────────┬───────────┬───────────┤");
// eslint-disable-next-line no-console
console.log("│ Test Name │ Nodes │ Runs │ Avg (ms) │ Min (ms) │ Max (ms) │");
// eslint-disable-next-line no-console
console.log("├──────────────────────────────┼──────────┼──────┼───────────┼───────────┼───────────┤");

for (const r of results) {
const name = r.name.padEnd(28);
const nodes = r.nodes.padStart(8);
const runs = r.iterations.toString().padStart(4);
const avg = r.avg.toFixed(3).padStart(9);
const min = r.min.toFixed(3).padStart(9);
const max = r.max.toFixed(3).padStart(9);
// eslint-disable-next-line no-console
console.log(`│ ${name} │ ${nodes} │ ${runs} │ ${avg} │ ${min} │ ${max} │`);
}

// eslint-disable-next-line no-console
console.log("└──────────────────────────────┴──────────┴──────┴───────────┴───────────┴───────────┘\n");
};

describe("Visit Performance", () => {
afterAll(() => {
printResultsTable();
});

it("should handle a deeply nested tree (depth=7, breadth=3)", async () => {
const { root, render } = await mountCanvas(
<DeepTree depth={7} breadth={3} />
);
await render();

const recorder = new Recorder();
const result = runBenchmark(
"Deep Tree (d=7, b=3)",
"~2000",
50,
() => visit(recorder, root.sg.children)
);
results.push(result);

expect(result.avg).toBeDefined();
});

it("should handle a wide tree (1500 siblings)", async () => {
const { root, render } = await mountCanvas(<WideTree count={1500} />);
await render();

const recorder = new Recorder();
const result = runBenchmark(
"Wide Tree (1500 siblings)",
"~4500",
50,
() => visit(recorder, root.sg.children)
);
results.push(result);

expect(result.avg).toBeDefined();
});

it("should handle a complex tree (60 groups x 25 shapes)", async () => {
const { root, render } = await mountCanvas(
<ComplexTree groups={60} shapesPerGroup={25} />
);
await render();

const recorder = new Recorder();
const result = runBenchmark(
"Complex Tree (60x25)",
"~3000",
50,
() => visit(recorder, root.sg.children)
);
results.push(result);

expect(result.avg).toBeDefined();
});

it("should handle large flat tree (3000 nodes)", async () => {
const { root, render } = await mountCanvas(<WideTree count={3000} />);
await render();

const recorder = new Recorder();
const result = runBenchmark(
"Large Flat Tree",
"~9000",
30,
() => visit(recorder, root.sg.children)
);
results.push(result);

expect(result.avg).toBeDefined();
});

it("benchmark: deeply nested with transforms (depth=11, breadth=2)", async () => {
const { root, render } = await mountCanvas(
<DeepTree depth={11} breadth={2} />
);
await render();

const recorder = new Recorder();
const result = runBenchmark(
"Deep Transforms (d=11, b=2)",
"~2000",
30,
() => visit(recorder, root.sg.children)
);
results.push(result);

expect(result.avg).toBeDefined();
});

it("should render correctly with a deep tree", async () => {
const img = await surface.draw(<DeepTree depth={5} breadth={3} />);
expect(img).toBeTruthy();
expect(img.width()).toBeGreaterThan(0);
expect(img.height()).toBeGreaterThan(0);
});
});
7 changes: 7 additions & 0 deletions packages/skia/src/skia/types/Recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
import type { AnimatedProps } from "../../renderer/processors/Animations/Animations";

import type { SkPicture } from "./Picture";
import type { Node } from "../../sksg/Node";

export interface BaseRecorder {
saveGroup(props?: AnimatedProps<Pick<DrawingNodeProps, "zIndex">>): void;
Expand Down Expand Up @@ -96,4 +97,10 @@ export interface JsiRecorder extends BaseRecorder {
play(picture: SkPicture): void;
applyUpdates(variables: SharedValue<unknown>[]): void;
reset(): void;
/**
* Native C++ visitor - traverses the node tree and records commands.
* Returns an array of shared values found during traversal.
*/
visit(root: Node[]): SharedValue<unknown>[];
getVariableCount(): number;
}
19 changes: 8 additions & 11 deletions packages/skia/src/sksg/Container.native.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import Rea from "../external/reanimated/ReanimatedProxy";
import type { Skia, SkPicture } from "../skia/types";
import type { Skia, SkPicture, JsiRecorder } from "../skia/types";
import { HAS_REANIMATED_3 } from "../external/reanimated/renderHelpers";
import type { JsiRecorder } from "../skia/types/Recorder";

import { ReanimatedRecorder } from "./Recorder/ReanimatedRecorder";
import { Container, StaticContainer } from "./StaticContainer";
import { visit } from "./Recorder/Visitor";

import "../skia/NativeSetup";
import "../views/api";
Expand Down Expand Up @@ -43,23 +40,23 @@ class NativeReanimatedContainer extends Container {
if (this.unmounted) {
return;
}
const recorder = new ReanimatedRecorder(this.Skia);
// Use native C++ visit for faster tree traversal
const recorder = this.Skia.Recorder();
const { nativeId, picture, Skia } = this;
visit(recorder, this.root);
const sharedValues = recorder.getSharedValues();
const sharedRecorder = recorder.getRecorder();
// Native visit returns the shared values it found
const sharedValues = recorder.visit(this.root);
// Draw first frame
Rea.executeOnUIRuntimeSync(() => {
"worklet";
const firstPicture = Skia.Picture.MakePicture(null)!;
nativeDrawOnscreen(nativeId, sharedRecorder, firstPicture);
nativeDrawOnscreen(nativeId, recorder, firstPicture);
})();
// Animate
if (sharedValues.length > 0) {
this.mapperId = Rea.startMapper(() => {
"worklet";
sharedRecorder.applyUpdates(sharedValues);
nativeDrawOnscreen(nativeId, sharedRecorder, picture);
recorder.applyUpdates(sharedValues);
nativeDrawOnscreen(nativeId, recorder, picture);
}, sharedValues);
}
}
Expand Down
8 changes: 7 additions & 1 deletion packages/skia/src/sksg/HostConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { NodeType } from "../dom/types";
import { shallowEq } from "../renderer/typeddash";

import type { Node } from "./Node";
import { bumpChildrenVersion } from "./Node";
import type { Container } from "./StaticContainer";

type EventPriority = number;
Expand Down Expand Up @@ -97,16 +98,18 @@ export const sksgHostConfig: SkiaHostConfig = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { children, ...props } = propsWithChildren as any;
debug("createInstance", type);
const instance = {
const instance: Node = {
type,
props,
children: [],
__skChildrenVersion: 0,
};
return instance;
},

appendInitialChild(parentInstance: Instance, child: Instance | TextInstance) {
parentInstance.children.push(child);
bumpChildrenVersion(parentInstance);
},

finalizeInitialChildren(
Expand Down Expand Up @@ -185,6 +188,9 @@ export const sksgHostConfig: SkiaHostConfig = {
type: instance.type,
props: { ...newProps },
children: keepChildren ? [...instance.children] : [],
__skChildrenVersion: keepChildren
? instance.__skChildrenVersion ?? 0
: 0,
};
},

Expand Down
Loading
Loading