Skip to content

Commit c2fe33c

Browse files
committed
Improve yoga when running with workers
This should prevent postMessage from being the bottleneck for calling yoga in a worker
1 parent 81ff0f5 commit c2fe33c

File tree

15 files changed

+1720
-356
lines changed

15 files changed

+1720
-356
lines changed

.changeset/every-words-check.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@plextv/react-native-lightning-example": patch
3+
"@plextv/react-lightning": patch
4+
"@plextv/react-lightning-plugin-flexbox": patch
5+
---
6+
7+
More optimizations with yoga workers

.vscode/settings.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
"source.fixAll.biome": "explicit"
66
},
77
"typescript.tsdk": "node_modules/typescript/lib",
8-
"cSpell.words": ["lightningjs", "threadx"],
8+
"cSpell.words": [
9+
"lightningjs",
10+
"threadx"
11+
],
912
"biome.enabled": true,
1013
"json.schemas": [
1114
{
12-
"fileMatch": ["manifest.json"],
15+
"fileMatch": [
16+
"manifest.json"
17+
],
1318
"url": "https://json.schemastore.org/chrome-manifest.json"
1419
}
1520
],
@@ -27,5 +32,6 @@
2732
},
2833
"[json]": {
2934
"editor.defaultFormatter": "biomejs.biome"
30-
}
31-
}
35+
},
36+
"vitest.disableWorkspaceWarning": true
37+
}

apps/react-native-lightning-example/src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ AppRegistry.runApplication('plex', {
194194
} as RenderOptions,
195195
pluginOptions: {
196196
flexbox: {
197-
useWebWorker: false,
197+
useWebWorker: true,
198198
},
199199
},
200200
});

packages/plugin-flexbox/src/LightningManager.ts

Lines changed: 50 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,16 @@ import type {
22
LightningElement,
33
LightningElementStyle,
44
} from '@plextv/react-lightning';
5-
import {
6-
UPDATE_HEIGHT,
7-
UPDATE_WIDTH,
8-
UPDATE_X,
9-
UPDATE_Y,
10-
} from './types/UpdateFlags';
115
import type { YogaOptions } from './types/YogaOptions';
6+
import { SimpleDataView } from './util/SimpleDataView';
127
import yoga from './yoga';
138

14-
const DEBOUNCE_DELAY = 1;
15-
169
/**
17-
* Manages the lifecycle of Yoga nodes for Lightning elements. This can only be done on the main thread and not the worker thread.
10+
* Manages the lifecycle of Yoga nodes for Lightning elements. This can only be
11+
* done on the main thread and not the worker thread.
1812
*/
1913
export class LightningManager {
2014
private _elements = new Map<number, LightningElement>();
21-
private _stylesToSend: Record<number, Partial<LightningElementStyle>> = {};
22-
private _needsRender: null | number = null;
2315

2416
public async init(yogaOptions?: YogaOptions) {
2517
await yoga.load(yogaOptions);
@@ -42,53 +34,51 @@ export class LightningManager {
4234
}
4335

4436
this._elements.delete(element.id);
45-
await yoga.instance.removeNode(element.id);
46-
this._render(element.id);
37+
yoga.instance.removeNode(element.id);
4738
}),
4839

4940
element.on('childAdded', async (child, index) => {
50-
await yoga.instance.addChildNode(element.id, child.id, index);
51-
52-
await this.applyStyle(element.id, element.props.style);
41+
yoga.instance.addChildNode(element.id, child.id, index);
42+
this.applyStyle(element.id, element.style);
5343
}),
5444

5545
element.on('childRemoved', async (child) => {
56-
await yoga.instance.removeNode(child.id);
57-
58-
this._render(element.id);
46+
// This will remove any pending worker style updates that haven't been sent
47+
yoga.instance.applyStyle(child.id, null, true);
48+
yoga.instance.removeNode(child.id);
5949
}),
6050

6151
element.on('stylesChanged', async () => {
62-
await this.applyStyle(element.id, element.props.style);
52+
this.applyStyle(element.id, element.props.style);
6353
}),
6454

6555
element.on('textureLoaded', async (node, event) => {
66-
// Text elements will already have its height and width set on the
67-
// node before loaded event is fired, so we need to set it on the yoga
68-
// node. If there's a maxWidth set, we should clamp the text to that size.
56+
// Text elements will already have its height and width set on the node
57+
// before loaded event is fired, so we need to set it on the yoga node.
58+
// If there's a maxWidth set, we should clamp the text to that size.
6959
// TODO: Get a proper return type
70-
const computedSize = await yoga.instance.getClampedSize(element.id);
71-
72-
if (
73-
computedSize !== null &&
74-
computedSize > 0 &&
75-
event.dimensions.width > computedSize
76-
) {
77-
node.contain = 'width';
78-
node.width = computedSize;
60+
if (element.isTextElement) {
61+
const computedSize = await yoga.instance.getClampedSize(element.id);
62+
63+
if (
64+
computedSize !== null &&
65+
computedSize > 0 &&
66+
event.dimensions.width > computedSize
67+
) {
68+
node.contain = 'width';
69+
node.width = computedSize;
70+
}
7971
}
8072

81-
await this.applyStyle(element.id, {
73+
this.applyStyle(element.id, {
8274
width: node.width,
8375
height: node.height,
8476
});
85-
86-
this._render(element.id);
8777
}),
8878
];
8979
}
9080

91-
public async applyStyle(
81+
public applyStyle(
9282
elementId: number,
9383
style?: Partial<LightningElementStyle> | null,
9484
skipRender = false,
@@ -97,55 +87,17 @@ export class LightningManager {
9787
return;
9888
}
9989

100-
// if (style) {
101-
// this._stylesToSend[elementId] = {
102-
// ...this._stylesToSend[elementId],
103-
// ...style,
104-
// };
105-
// } else {
106-
// delete this._stylesToSend[elementId];
107-
// }
108-
109-
// if (!skipRender) {
110-
// this._needsRender = elementId;
111-
// }
112-
11390
if (style) {
114-
await yoga.instance.applyStyle(elementId, style);
115-
}
116-
117-
if (!skipRender) {
118-
await this._render(elementId);
91+
yoga.instance.applyStyle(elementId, style, skipRender);
11992
}
120-
121-
// return this._sendStyles();
12293
}
12394

124-
private _sendStyles = debounced(async (): Promise<void> => {
125-
if (Object.keys(this._stylesToSend).length === 0) {
126-
return;
127-
}
128-
129-
await yoga.instance.applyStyles(this._stylesToSend);
130-
this._stylesToSend = {};
131-
132-
if (this._needsRender !== null) {
133-
await this._render(this._needsRender);
134-
this._needsRender = null;
135-
}
136-
}, DEBOUNCE_DELAY);
137-
13895
private _applyUpdates = (buffer: ArrayBuffer) => {
139-
const dataView = new DataView(buffer);
96+
const dataView = new SimpleDataView(buffer);
14097

14198
// See YogaManagerWorker.ts for the structure of the updates
142-
const numItems = dataView.getUint32(0);
143-
let offset = 4;
144-
145-
for (let i = 0; i < numItems; i++) {
146-
const elementId = dataView.getUint32(offset);
147-
offset += 4;
148-
99+
while (dataView.hasSpace(1)) {
100+
const elementId = dataView.readUint32();
149101
const el = this._elements.get(elementId);
150102

151103
if (!el) {
@@ -168,65 +120,37 @@ export class LightningManager {
168120
el.style.transform?.translateY === undefined;
169121
}
170122

171-
const flags = dataView.getUint8(offset);
172-
offset += 1;
173-
174-
if (flags & UPDATE_X) {
175-
if (!skipX) {
176-
dirty = el.setNodeProp('x', dataView.getInt16(offset)) || dirty;
177-
}
178-
179-
offset += 2;
123+
if (!skipX) {
124+
dirty = el.setNodeProp('x', dataView.readInt16()) || dirty;
125+
} else {
126+
// If the x is skipped, we still need to read the value to maintain the
127+
// correct offset in the data view.
128+
dataView.moveBy(2);
180129
}
181130

182-
if (flags & UPDATE_Y) {
183-
if (!skipY) {
184-
dirty = el.setNodeProp('y', dataView.getInt16(offset)) || dirty;
185-
}
186-
187-
offset += 2;
131+
if (!skipY) {
132+
dirty = el.setNodeProp('y', dataView.readInt16()) || dirty;
133+
} else {
134+
// Same as above
135+
dataView.moveBy(2);
188136
}
189137

190-
if (flags & UPDATE_WIDTH) {
191-
dirty = el.setNodeProp('width', dataView.getUint16(offset)) || dirty;
192-
offset += 2;
138+
const width = dataView.readUint16();
139+
const height = dataView.readUint16();
140+
141+
// If width is 0, we should not set it on the node, as it will cause
142+
// layout issues.
143+
if (width !== 0) {
144+
dirty = el.setNodeProp('width', width) || dirty;
193145
}
194146

195-
if (flags & UPDATE_HEIGHT) {
196-
dirty = el.setNodeProp('height', dataView.getUint16(offset)) || dirty;
197-
offset += 2;
147+
if (height !== 0) {
148+
dirty = el.setNodeProp('height', height) || dirty;
198149
}
199150

200151
if (dirty) {
201152
el.emitLayoutEvent();
202153
}
203154
}
204155
};
205-
206-
private async _render(elementId: number) {
207-
yoga.instance.queueRender(elementId);
208-
}
209-
}
210-
211-
function debounced<T extends (...args: unknown[]) => void | Promise<void>>(
212-
fn: T,
213-
delay: number,
214-
): T {
215-
let timeout: ReturnType<typeof setTimeout> | null = null;
216-
let lastArgs: unknown[];
217-
218-
const debouncedFn = function (this: unknown, ...args: unknown[]) {
219-
lastArgs = args;
220-
221-
if (timeout) {
222-
clearTimeout(timeout);
223-
}
224-
225-
timeout = setTimeout(() => {
226-
timeout = null;
227-
fn.apply(this, lastArgs);
228-
}, delay);
229-
};
230-
231-
return debouncedFn as T;
232156
}

packages/plugin-flexbox/src/YogaManager.spec.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import type { LightningElementStyle } from '@plextv/react-lightning';
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3-
import {
4-
UPDATE_HEIGHT,
5-
UPDATE_WIDTH,
6-
UPDATE_X,
7-
UPDATE_Y,
8-
} from './types/UpdateFlags';
93
import type { YogaOptions } from './types/YogaOptions';
4+
import { SimpleDataView } from './util/SimpleDataView';
105
import { YogaManager } from './YogaManager';
116

127
// Mock the yoga-layout/load module
@@ -315,29 +310,15 @@ describe('YogaManager', () => {
315310
yogaManager.on('render', (arrayBuffer) => {
316311
expect(arrayBuffer.byteLength).toBeGreaterThan(0); // Ensure some data is present
317312

318-
const dataView = new DataView(arrayBuffer);
319-
let offset = 0;
320-
321-
// Number of nodes
322-
expect(dataView.getUint32(offset)).toBe(numNodes);
323-
offset += 4;
313+
const dataView = new SimpleDataView(arrayBuffer);
324314

325315
// Check each node's data
326316
for (let i = 0; i < numNodes; i++) {
327-
expect(dataView.getUint32(offset)).toBe(i); // Node ID
328-
offset += 4;
329-
expect(dataView.getUint8(offset)).toBe(
330-
UPDATE_X | UPDATE_Y | UPDATE_WIDTH | UPDATE_HEIGHT,
331-
); // Flags
332-
offset += 1;
333-
expect(dataView.getInt16(offset)).toBe(10); // x
334-
offset += 2;
335-
expect(dataView.getInt16(offset)).toBe(20); // y
336-
offset += 2;
337-
expect(dataView.getInt16(offset)).toBe(100); // width
338-
offset += 2;
339-
expect(dataView.getInt16(offset)).toBe(50); // height
340-
offset += 2;
317+
expect(dataView.readUint32()).toBe(i); // Node ID
318+
expect(dataView.readInt16()).toBe(10); // x
319+
expect(dataView.readInt16()).toBe(20); // y
320+
expect(dataView.readInt16()).toBe(100); // width
321+
expect(dataView.readInt16()).toBe(50); // height
341322
}
342323

343324
resolve();

0 commit comments

Comments
 (0)