Skip to content

Commit

Permalink
Merge pull request #717 from Shopify/feature/remove-pull-from-reconciler
Browse files Browse the repository at this point in the history
Remove pull from reconciler
  • Loading branch information
chrfalch authored Aug 29, 2022
2 parents 41ef248 + 9284ddd commit db38ab0
Show file tree
Hide file tree
Showing 14 changed files with 397 additions and 90 deletions.
6 changes: 6 additions & 0 deletions package/cpp/api/JsiSkPaint.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ class JsiSkPaint : public JsiSkWrappingSharedPtrHostObject<SkPaint> {
runtime, std::make_shared<JsiSkPaint>(getContext(), SkPaint(*paint)));
}

JSI_HOST_FUNCTION(reset) {
getObject()->reset();
return jsi::Value::undefined();
}

JSI_HOST_FUNCTION(getColor) {
return JsiSkColor::toValue(runtime, getObject()->getColor());
}
Expand Down Expand Up @@ -144,6 +149,7 @@ class JsiSkPaint : public JsiSkWrappingSharedPtrHostObject<SkPaint> {
}

JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkPaint, copy),
JSI_EXPORT_FUNC(JsiSkPaint, reset),
JSI_EXPORT_FUNC(JsiSkPaint, getColor),
JSI_EXPORT_FUNC(JsiSkPaint, getStrokeCap),
JSI_EXPORT_FUNC(JsiSkPaint, getStrokeJoin),
Expand Down
26 changes: 19 additions & 7 deletions package/src/renderer/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SkiaView, useDrawCallback } from "../views";
import type { TouchHandler } from "../views";
import { useValue } from "../values/hooks/useValue";
import { Skia } from "../skia/Skia";
import type { SkiaValue } from "../values";

import { debug as hostDebug, skHostConfig } from "./HostConfig";
// import { debugTree } from "./nodes";
Expand All @@ -38,8 +39,7 @@ skiaReconciler.injectIntoDevTools({
const render = (element: ReactNode, root: OpaqueRoot, container: Container) => {
skiaReconciler.updateContainer(element, root, null, () => {
hostDebug("updateContainer");

container.depMgr.subscribe();
container.depMgr.update();
});
};

Expand All @@ -60,9 +60,19 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
const [tick, setTick] = useState(0);
const redraw = useCallback(() => setTick((t) => t + 1), []);

const registerValues = useCallback(
(values: Array<SkiaValue<unknown>>) => {
if (ref.current === null) {
throw new Error("Canvas ref is not set");
}
return ref.current.registerValues(values);
},
[ref]
);

const container = useMemo(
() => new Container(new DependencyManager(ref), redraw),
[redraw, ref]
() => new Container(new DependencyManager(registerValues), redraw),
[redraw, registerValues]
);

const root = useMemo(
Expand All @@ -78,6 +88,8 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
);
}, [children, root, redraw, container, canvasCtx]);

const paint = useMemo(() => Skia.Paint(), []);

// Draw callback
const onDraw = useDrawCallback(
(canvas, info) => {
Expand All @@ -92,7 +104,7 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
) {
canvasCtx.size.current = { width, height };
}
const paint = Skia.Paint();
paint.reset();
const ctx = {
width,
height,
Expand All @@ -101,7 +113,7 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
paint,
opacity: 1,
ref,
center: Skia.Point(width / 2, height / 2),
center: { x: width / 2, y: height / 2 },
Skia,
};
container.draw(ctx);
Expand All @@ -112,7 +124,7 @@ export const Canvas = forwardRef<SkiaView, CanvasProps>(
useEffect(() => {
return () => {
skiaReconciler.updateContainer(null, root, null, () => {
container.depMgr.unsubscribe();
container.depMgr.remove();
});
};
}, [container, root]);
Expand Down
209 changes: 170 additions & 39 deletions package/src/renderer/DependencyManager.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,193 @@
import type { RefObject } from "react";

import type { SkiaView } from "../views";
import type { SkiaValue } from "../values";

import { isSelector, isValue } from "./processors";
import type { Node } from "./nodes";
import type { AnimatedProps } from "./processors";
import { isSelector, isValue } from "./processors";
import { mapKeys } from "./typeddash";

type Unsubscribe = () => void;
type Props = { [key: string]: unknown };
type Mutator = (value: unknown) => void;

type SubscriptionState = {
nodes: Map<Node, Mutator[]>;
unsubscribe: null | Unsubscribe;
};

export class DependencyManager {
ref: RefObject<SkiaView>;
subscriptions: Map<
Node,
{ values: SkiaValue<unknown>[]; unsubscribe: null | Unsubscribe }
> = new Map();

constructor(ref: RefObject<SkiaView>) {
this.ref = ref;
}
registerValues: (values: Array<SkiaValue<unknown>>) => () => void;
subscriptions: Map<SkiaValue<unknown>, SubscriptionState> = new Map();
unregisterDependantValues: null | Unsubscribe = null;

unSubscribeNode(node: Node) {
const subscription = this.subscriptions.get(node);
if (subscription && subscription.unsubscribe) {
subscription.unsubscribe();
}
this.subscriptions.delete(node);
constructor(
registerValues: (values: Array<SkiaValue<unknown>>) => () => void
) {
this.registerValues = registerValues;
}

subscribeNode(node: Node, props: Props) {
const values = Object.values(props)
.filter((v) => isValue(v) || isSelector(v))
.map((v) => (isSelector(v) ? v.value : (v as SkiaValue<unknown>)));
/**
* Call to unsubscribe all value listeners from the given node based
* on the current list of subscriptions for the node. This function
* is typically called when the node is unmounted or when one or more
* properties have changed.
* @param node Node to unsubscribe value listeners from
*/
unsubscribeNode(node: Node) {
const subscriptions = Array.from(this.subscriptions.values()).filter((p) =>
p.nodes.has(node)
);

if (values.length > 0) {
this.subscriptions.set(node, { values, unsubscribe: null });
if (subscriptions) {
subscriptions.forEach((si) => {
// Delete node from subscription
si.nodes.delete(node);

// Remove subscription if there are no listeneres left on the value
if (si.nodes.size === 0) {
// There are no more nodes subscribing to this value, we can call
// unsubscribe on it.
if (!si.unsubscribe) {
throw new Error("Failed to unsubscribe to value subscription");
}
si.unsubscribe && si.unsubscribe();

// Remove from subscription states as well
const element = Array.from(this.subscriptions.entries()).find(
([_, sub]) => sub === si
);
if (!element) {
throw new Error("Failed to find value subscription");
}
if (!this.subscriptions.delete(element[0])) {
throw new Error("Failed to delete value subscription");
}
}
});
}
}

subscribe() {
if (this.ref.current === null) {
throw new Error("Canvas ref is not set");
/**
* Adds listeners to the provided values so that the node is notified
* when a value changes. This is done in an optimized way so that this
* class only needs to listen to the value once and then forwards the
* change to the node and its listener. This method is typically called
* when the node is mounted and when one or more props on the node changes.
* @param node Node to subscribe to value changes for
* @param props Node's properties
* @param onResolveProp Callback when a property value changes
*/
subscribeNode<P extends Record<string, unknown>>(
node: Node,
props: AnimatedProps<P>
) {
// Get mutators from node's properties
const propSubscriptions = initializePropertySubscriptions(node, props);
if (propSubscriptions.length === 0) {
return;
}
this.subscriptions.forEach((subscription) => {
if (subscription.unsubscribe === null) {
subscription.unsubscribe = this.ref.current!.registerValues(
subscription.values
);

// Install all mutators for the node
propSubscriptions.forEach((ps) => {
// Do we already have a state for this SkiaValue
let subscriptionState = this.subscriptions.get(ps.value);
if (!subscriptionState) {
// Let's create a new subscription state for the skia value
subscriptionState = {
nodes: new Map(),
unsubscribe: null,
};
// Add single subscription to the new value
subscriptionState.unsubscribe = ps.value.addListener((v) => {
subscriptionState!.nodes.forEach((mutators) =>
mutators.forEach((m) => m(v))
);
});
this.subscriptions.set(ps.value, subscriptionState);
}
// subscription mutators
subscriptionState.nodes.set(
node,
propSubscriptions
.filter((m) => m.value === ps.value)
.map((m) => m.mutator)
);
});
}

unsubscribe() {
this.subscriptions.forEach(({ unsubscribe }) => {
if (unsubscribe) {
unsubscribe();
}
/**
* Called when the hosting container is mounted or updated. This ensures that we have
* a ref to the underlying SkiaView so that we can registers redraw listeners
* on values used in the current View automatically.
*/
update() {
// Remove any previous registrations
if (this.unregisterDependantValues) {
this.unregisterDependantValues();
}

// Register redraw requests on the SkiaView for each unique value
this.unregisterDependantValues = this.registerValues(
Array.from(this.subscriptions.keys())
);
}

/**
* Called when the hosting container is unmounted or recreated. This ensures that we remove
* all subscriptions to Skia values so that we don't have any listeners left after
* the component is removed.
*/
remove() {
// 1) Unregister redraw requests
if (this.unregisterDependantValues) {
this.unregisterDependantValues();
this.unregisterDependantValues = null;
}

// 2) Unregister nodes
Array.from(this.subscriptions.values()).forEach((si) => {
Array.from(si.nodes.keys()).forEach((node) => this.unsubscribeNode(node));
});

// 3) Clear the rest of the subscriptions
this.subscriptions.clear();
}
}

const initializePropertySubscriptions = <P,>(
node: Node<P>,
props: AnimatedProps<P>
) => {
const nodePropSubscriptions: Array<{
value: SkiaValue<unknown>;
mutator: Mutator;
}> = [];

mapKeys(props).forEach((key) => {
const propvalue = props[key];

if (isValue(propvalue)) {
// Subscribe to changes
nodePropSubscriptions.push({
value: propvalue,
mutator: (v) => (node.resolvedProps[key] = v as P[typeof key]),
});
// Set initial value
node.resolvedProps[key] = (propvalue as SkiaValue<P[typeof key]>).current;
} else if (isSelector(propvalue)) {
// Subscribe to changes
nodePropSubscriptions.push({
value: propvalue.value,
mutator: (v) =>
(node.resolvedProps[key] = propvalue.selector(v) as P[typeof key]),
});
// Set initial value
node.resolvedProps[key] = propvalue.selector(
propvalue.value.current
) as P[typeof key];
} else {
// Set initial value
node.resolvedProps[key] = propvalue as P[typeof key];
}
});

return nodePropSubscriptions;
};
2 changes: 1 addition & 1 deletion package/src/renderer/HostConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const removeNode = (parent: Node, child: Node) => {
bustBranchMemoization(parent);
const index = parent.children.indexOf(child);
parent.children.splice(index, 1);
child.depMgr.unSubscribeNode(child);
child.depMgr.unsubscribeNode(child);
// unsubscribe to all children as well
for (const c of child.children) {
removeNode(child, c);
Expand Down
Loading

0 comments on commit db38ab0

Please sign in to comment.