FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

perf(core): commit without a batch for nodes that cannot observe one · NativeScript/NativeScript@6aad60a · GitHub

Repository navigation

Commit 6aad60a

Browse files
committed
perf(core): commit without a batch for nodes that cannot observe one
A node using the default commit hook with no aggregate invalidations pending cannot reach the batch, so the commit sweeps the dirty set directly instead of building one.
1 parent 487a4d8 commit 6aad60a

3 files changed

Lines changed: 203 additions & 7 deletions

File tree

‎packages/core/ui/core/native-updates/native-updates.spec.ts‎

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, beforeEach } from 'vitest';
1+
import { describe, it, expect, beforeEach, vi } from 'vitest';
22

33
import { View } from '../view';
44
import { Style } from '../../styling/style';
@@ -168,6 +168,81 @@ describe('commitNativeUpdates', () => {
168168
});
169169
});
170170

171+
describe('committing without a batch', () => {
172+
it('builds no batch for a node whose class can neither order nor observe one', () => {
173+
const plain: any = loaded(new TestView());
174+
const ordered: any = loaded(new OrderedView());
175+
const applyRemaining = vi.spyOn(NativeUpdateBatch.prototype, '_applyRemaining');
176+
177+
try {
178+
plain._batchUpdate(() => {
179+
plain.two = 'b';
180+
plain.one = 'a';
181+
});
182+
NativeUpdates.batch(() => {
183+
plain.one = 'c';
184+
});
185+
plain.callUnloaded();
186+
plain.two = 'd';
187+
plain.flushNativeUpdates({ force: true });
188+
plain.callLoaded();
189+
190+
expect(log).toEqual(['two=b', 'one=a', 'one=c', 'two=d']);
191+
expect(applyRemaining).not.toHaveBeenCalled();
192+
193+
log.length = 0;
194+
ordered._batchUpdate(() => {
195+
ordered.two = 'b';
196+
ordered.one = 'a';
197+
});
198+
199+
expect(log).toEqual(['commit', 'two=b', 'one=a']);
200+
expect(applyRemaining).toHaveBeenCalledTimes(1);
201+
} finally {
202+
applyRemaining.mockRestore();
203+
}
204+
});
205+
206+
it('sweeps a mount in the same order the batch path does', () => {
207+
function mounted(view: any): string[] {
208+
log.length = 0;
209+
view.two = 'b';
210+
view.style.three = 'c';
211+
view.one = 'a';
212+
view._setupUI({});
213+
view.callLoaded();
214+
215+
return log.filter((entry) => entry !== 'commit');
216+
}
217+
218+
const withoutBatch = mounted(new TestView());
219+
const withBatch = mounted(new OrderedView());
220+
221+
expect(withoutBatch).toEqual(['two=b', 'one=a', 'three=c']);
222+
expect(withBatch).toEqual(withoutBatch);
223+
});
224+
225+
it('sweeps a dirty set in the same order the batch path does', () => {
226+
function reloaded(view: any): string[] {
227+
loaded(view);
228+
view.callUnloaded();
229+
view.two = 'b';
230+
view.style.three = 'c';
231+
view.one = 'a';
232+
log.length = 0;
233+
view.callLoaded();
234+
235+
return log.filter((entry) => entry !== 'commit');
236+
}
237+
238+
const withoutBatch = reloaded(new TestView());
239+
const withBatch = reloaded(new OrderedView());
240+
241+
expect(withoutBatch).toEqual(['two=b', 'three=c', 'one=a']);
242+
expect(withBatch).toEqual(withoutBatch);
243+
});
244+
});
245+
171246
describe('the batch', () => {
172247
function commitOf(view: TestView): NativeUpdateBatch {
173248
return view.commits[view.commits.length - 1];

‎packages/core/ui/core/properties/index.ts‎

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1554,7 +1554,24 @@ function applyNativeUpdate(batch: NativeUpdateBatch, entry: NativeUpdateEntry):
15541554
* `ViewBase.flushNativeUpdates` to push what is pending.
15551555
*/
15561556
export const initNativeView = profile('"properties".initNativeView', function initNativeView(view: ViewBase): void {
1557-
const isMount = view._suspendedUpdates === undefined;
1557+
const dirty = view._suspendedUpdates;
1558+
const isMount = dirty === undefined;
1559+
1560+
if (view._pendingInvalidations === undefined && view.commitNativeUpdates === defaultCommitNativeUpdates) {
1561+
// Nothing on this node can reach a batch, so the sweep runs straight off the dirty set,
1562+
// which is dropped first as on the batch path.
1563+
view._suspendedUpdates = {};
1564+
view._pendingPrevious = undefined;
1565+
1566+
if (isMount) {
1567+
applyAllNativeSetters(view);
1568+
} else {
1569+
applyDirtyNativeSetters(view, dirty);
1570+
}
1571+
1572+
return;
1573+
}
1574+
15581575
const batch = new NativeUpdateBatch(view, isMount, collectNativeUpdateEntries(view, isMount), view._pendingPrevious, applyNativeUpdate);
15591576

15601577
// Dropped before the commit runs so that a write made from a handler queues against a fresh set.
@@ -1612,15 +1629,18 @@ function applyMountedNativeSetter(view: ViewBase, property: PropertyInterface):
16121629
* @deprecated Superseded by `ViewBase.commitNativeUpdates`, which applies the same dirty set
16131630
* through a `NativeUpdateBatch` the class can reorder.
16141631
*/
1615-
export function applyPendingNativeSetters(view: ViewBase): void {
1632+
function applyDirtyNativeSetters(view: ViewBase, dirty: ViewBase['_suspendedUpdates']): void {
16161633
// TODO: Check what happens if a view was suspended and its value was reset, or set back to default!
1617-
const suspendedUpdates = view._suspendedUpdates;
1618-
for (const propertyName in suspendedUpdates) {
1619-
if (!HAS_OWN.call(suspendedUpdates, propertyName)) continue;
1620-
applyPendingNativeSetter(view, <PropertyInterface>suspendedUpdates[propertyName]);
1634+
for (const propertyName in dirty) {
1635+
if (!HAS_OWN.call(dirty, propertyName)) continue;
1636+
applyPendingNativeSetter(view, <PropertyInterface>dirty[propertyName]);
16211637
}
16221638
}
16231639

1640+
export function applyPendingNativeSetters(view: ViewBase): void {
1641+
applyDirtyNativeSetters(view, view._suspendedUpdates);
1642+
}
1643+
16241644
/**
16251645
* @deprecated Superseded by `ViewBase.commitNativeUpdates` with a batch whose `isMount` is set.
16261646
*/
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import { View } from '../view';
4+
import { Property } from './index';
5+
6+
/**
7+
* What a commit costs per node, where the batch is built. The numbers are printed rather than
8+
* asserted - the assertion is only that every pending value arrived.
9+
*/
10+
const COMMIT_PROPERTIES = 8;
11+
const RELOAD_PROPERTIES = 4;
12+
13+
let calls = 0;
14+
15+
class TestView extends View {
16+
createNativeView(): Object {
17+
return {};
18+
}
19+
20+
public _addViewToNativeVisualTree(): boolean {
21+
return true;
22+
}
23+
}
24+
25+
const propertyNames: string[] = [];
26+
for (let i = 0; i < COMMIT_PROPERTIES; i++) {
27+
const property = new Property<TestView, number>({ name: `commit${i}`, defaultValue: -1 });
28+
property.register(TestView);
29+
(TestView.prototype as any)[property.setNative] = function () {
30+
calls++;
31+
};
32+
propertyNames.push(property.name);
33+
}
34+
35+
function write(view: any, count: number, value: number): void {
36+
for (let i = 0; i < count; i++) {
37+
view[propertyNames[i]] = value;
38+
}
39+
}
40+
41+
function loadedView(): any {
42+
const view: any = new TestView();
43+
view._setupUI({});
44+
view.callLoaded();
45+
46+
return view;
47+
}
48+
49+
function measure(label: string, iterations: number, unit: string, callsEach: number, run: (iterations: number) => void): void {
50+
run(Math.max(1, Math.round(iterations / 10)));
51+
52+
calls = 0;
53+
const started = performance.now();
54+
run(iterations);
55+
const elapsed = performance.now() - started;
56+
57+
console.log(`commit ${label}: ${iterations} ${unit} in ${elapsed.toFixed(1)}ms (${((elapsed * 1000) / iterations).toFixed(2)}us each)`);
58+
expect(calls).toBe(callsEach * iterations);
59+
}
60+
61+
describe('commit cost', () => {
62+
it('loads views carrying pending properties', () => {
63+
measure('load', 2000, 'views', COMMIT_PROPERTIES, (iterations) => {
64+
for (let i = 0; i < iterations; i++) {
65+
const view: any = new TestView();
66+
write(view, COMMIT_PROPERTIES, i);
67+
view._setupUI({});
68+
view.callLoaded();
69+
}
70+
});
71+
});
72+
73+
it('reloads views written to while unloaded', () => {
74+
const views: any[] = [];
75+
for (let i = 0; i < 2200; i++) {
76+
views.push(loadedView());
77+
}
78+
let next = 0;
79+
80+
measure('reload', 2000, 'views', RELOAD_PROPERTIES, (iterations) => {
81+
for (let i = 0; i < iterations; i++) {
82+
const view = views[next++ % views.length];
83+
view.callUnloaded();
84+
write(view, RELOAD_PROPERTIES, i);
85+
view.callLoaded();
86+
}
87+
});
88+
});
89+
90+
it('commits batched writes on a live view', () => {
91+
const view = loadedView();
92+
93+
measure('_batchUpdate', 5000, 'batches', COMMIT_PROPERTIES, (iterations) => {
94+
for (let i = 0; i < iterations; i++) {
95+
view._batchUpdate(() => {
96+
write(view, COMMIT_PROPERTIES, i);
97+
});
98+
}
99+
});
100+
});
101+
});

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL