Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 1eb3706

Browse filesBrowse files
authored
perf(core): hot-path optimizations across events, properties, CSS matching and iOS layout (#11307)
1 parent 5bb52e7 commit 1eb3706
Copy full SHA for 1eb3706

18 files changed

+540-78Lines changed: 540 additions & 78 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎.eslintrc.json‎

Copy file name to clipboardExpand all lines: .eslintrc.json
+5-2Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,18 @@
3939
"@typescript-eslint/no-this-alias": "warn",
4040
"@typescript-eslint/no-namespace": "off",
4141
"@typescript-eslint/no-inferrable-types": "off",
42-
"@typescript-eslint/no-extra-semi": "error",
42+
"@typescript-eslint/no-empty-object-type": "off",
43+
"@typescript-eslint/no-unsafe-function-type": "off",
44+
"@typescript-eslint/no-wrapper-object-types": "off",
45+
"@typescript-eslint/no-unsafe-declaration-merging": "off",
46+
"@typescript-eslint/no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true }],
4347
"no-extra-semi": "off"
4448
}
4549
},
4650
{
4751
"files": ["*.js", "*.jsx"],
4852
"extends": ["plugin:@nx/javascript"],
4953
"rules": {
50-
"@typescript-eslint/no-extra-semi": "error",
5154
"no-extra-semi": "off"
5255
}
5356
},
Collapse file

‎packages/core/.eslintrc.json‎

Copy file name to clipboardExpand all lines: packages/core/.eslintrc.json
+1-8Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
{
22
"extends": "../../.eslintrc.json",
33
"rules": {},
4-
"ignorePatterns": ["!**/*", "**/global-types.d.ts", "**/node_modules/**/*", "**/__tests__/**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"],
4+
"ignorePatterns": ["!**/*", "**/global-types.d.ts", "**/node_modules/**/*", "**/__tests__/**/*", "**/platforms/**/*", "**/css/lib/**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"],
55
"overrides": [
6-
{
7-
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
8-
"parserOptions": {
9-
"project": ["packages/core/tsconfig.*?.json"]
10-
},
11-
"rules": {}
12-
},
136
{
147
"files": ["*.ts", "*.tsx"],
158
"rules": {}
Collapse file
+87Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { bench, describe } from 'vitest';
2+
import { parse } from '../../css/reworkcss.js';
3+
import { AttributeSelector, createSelector, RuleSet, StyleSheetSelectorScope } from '../../ui/styling/css-selector';
4+
import { _populateRules } from '../../ui/styling/style-scope';
5+
6+
function createScope(css: string): StyleSheetSelectorScope<any> {
7+
const parsed = parse(css, { source: 'css-selector.bench.ts' });
8+
const rulesets: RuleSet[] = [];
9+
_populateRules(parsed.stylesheet.rules, rulesets, []);
10+
11+
return new StyleSheetSelectorScope(rulesets);
12+
}
13+
14+
// Build a stylesheet shaped like a typical themed app: universal rules, type
15+
// rules, many class rules, ids and a couple of media queries.
16+
function buildStylesheet(): string {
17+
const parts: string[] = ['* { font-family: sans-serif; }', 'view { color: black; }'];
18+
19+
const types = ['button', 'label', 'stacklayout', 'gridlayout', 'image', 'textfield', 'listview', 'scrollview'];
20+
for (const type of types) {
21+
parts.push(`${type} { color: red; }`);
22+
parts.push(`${type}.accent { color: blue; }`);
23+
}
24+
25+
for (let i = 0; i < 60; i++) {
26+
parts.push(`.cls-${i} { margin: ${i}; }`);
27+
parts.push(`.cls-${i} .child-${i} { padding: ${i}; }`);
28+
}
29+
30+
for (let i = 0; i < 10; i++) {
31+
parts.push(`#id-${i} { color: green; }`);
32+
}
33+
34+
parts.push('@media only screen and (min-width: 400) { button { color: purple; } .cls-1 { color: orange; } }');
35+
parts.push('@media only screen and (min-width: 400) and (max-width: 5000) { label { color: yellow; } }');
36+
37+
return parts.join('\n');
38+
}
39+
40+
const scope = createScope(buildStylesheet());
41+
42+
const buttonNode = {
43+
cssType: 'button',
44+
id: 'id-5',
45+
cssClasses: new Set(['accent', 'cls-1', 'cls-2', 'cls-30']),
46+
cssPseudoClasses: new Set<string>(),
47+
};
48+
49+
const plainLabelNode = {
50+
cssType: 'label',
51+
cssClasses: new Set<string>(),
52+
cssPseudoClasses: new Set<string>(),
53+
};
54+
55+
describe('StyleSheetSelectorScope.query', () => {
56+
bench('query - button with id and 4 classes', () => {
57+
scope.query(buttonNode);
58+
});
59+
60+
bench('query - plain label', () => {
61+
scope.query(plainLabelNode);
62+
});
63+
});
64+
65+
describe('selector match', () => {
66+
const sequenceSelector = createSelector('button.accent.cls-1');
67+
const sequenceNode = { cssType: 'button', cssClasses: new Set(['accent', 'cls-1']) };
68+
69+
bench('SimpleSelectorSequence.match', () => {
70+
sequenceSelector.match(sequenceNode);
71+
});
72+
73+
const complexSelector = createSelector('stacklayout > label.title');
74+
const parentNode = { cssType: 'stacklayout', cssClasses: new Set<string>() };
75+
const childNode = { cssType: 'label', cssClasses: new Set(['title']), parent: parentNode };
76+
77+
bench('ComplexSelector.match (child combinator)', () => {
78+
complexSelector.match(childNode);
79+
});
80+
81+
const attributeSelector = new AttributeSelector('testAttr', 'equals', 'SOME-value', true);
82+
const attributeNode = { cssType: 'button', testAttr: 'some-VALUE' };
83+
84+
bench('AttributeSelector.match (ignoreCase)', () => {
85+
attributeSelector.match(attributeNode);
86+
});
87+
});
Collapse file
+78Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { bench, describe } from 'vitest';
2+
import { Observable, EventData } from '../../data/observable';
3+
4+
class TestObservable extends Observable {}
5+
6+
function noop(_data: EventData) {
7+
// listener body intentionally empty
8+
}
9+
10+
describe('Observable.notify', () => {
11+
const singleListener = new TestObservable();
12+
singleListener.on('tick', noop);
13+
14+
bench('notify - single listener (no thisArg)', () => {
15+
singleListener.notify({ eventName: 'tick', object: singleListener });
16+
});
17+
18+
const singleListenerThisArg = new TestObservable();
19+
const ctx = { name: 'ctx' };
20+
singleListenerThisArg.on('tick', noop, ctx);
21+
22+
bench('notify - single listener (with thisArg)', () => {
23+
singleListenerThisArg.notify({ eventName: 'tick', object: singleListenerThisArg });
24+
});
25+
26+
const multiListener = new TestObservable();
27+
multiListener.on('tick', noop, { id: 1 });
28+
multiListener.on('tick', noop, { id: 2 });
29+
multiListener.on('tick', noop, { id: 3 });
30+
31+
bench('notify - three listeners (with thisArg)', () => {
32+
multiListener.notify({ eventName: 'tick', object: multiListener });
33+
});
34+
35+
const noListeners = new TestObservable();
36+
37+
bench('notify - no listeners', () => {
38+
noListeners.notify({ eventName: 'tick', object: noListeners });
39+
});
40+
41+
const propertyChangeTarget = new TestObservable();
42+
propertyChangeTarget.on(Observable.propertyChangeEvent, noop);
43+
44+
bench('notifyPropertyChange', () => {
45+
propertyChangeTarget.notifyPropertyChange('value', 1, 0);
46+
});
47+
});
48+
49+
const ctxLast = { id: 'last' };
50+
51+
describe('Observable listener management', () => {
52+
const observable = new TestObservable();
53+
54+
bench('hasListeners - present', () => {
55+
observable.hasListeners('registered');
56+
});
57+
58+
observable.on('registered', noop);
59+
60+
bench('hasListeners - absent', () => {
61+
observable.hasListeners('unregistered');
62+
});
63+
64+
bench('on/off cycle', () => {
65+
observable.on('cycle', noop);
66+
observable.off('cycle', noop);
67+
});
68+
69+
const manyListeners = new TestObservable();
70+
for (let i = 0; i < 10; i++) {
71+
manyListeners.on('busy', noop, { id: i });
72+
}
73+
74+
bench('on/off cycle - 10 existing listeners', () => {
75+
manyListeners.on('busy', noop, ctxLast);
76+
manyListeners.off('busy', noop, ctxLast);
77+
});
78+
});
Collapse file
+30Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { bench, describe } from 'vitest';
2+
import { isCssWideKeyword, isResetValue, unsetValue } from '../../ui/core/properties/property-shared';
3+
4+
const objectValue = { some: 'value' };
5+
6+
describe('property value helpers', () => {
7+
bench('isResetValue - number value', () => {
8+
isResetValue(42);
9+
});
10+
11+
bench('isResetValue - object value', () => {
12+
isResetValue(objectValue);
13+
});
14+
15+
bench('isResetValue - regular string value', () => {
16+
isResetValue('center');
17+
});
18+
19+
bench('isResetValue - unsetValue', () => {
20+
isResetValue(unsetValue);
21+
});
22+
23+
bench('isCssWideKeyword - number value', () => {
24+
isCssWideKeyword(42);
25+
});
26+
27+
bench('isCssWideKeyword - regular string value', () => {
28+
isCssWideKeyword('center');
29+
});
30+
});
Collapse file

‎packages/core/css/CSS3Parser.ts‎

Copy file name to clipboardExpand all lines: packages/core/css/CSS3Parser.ts
+3-2Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,8 @@ export class CSS3Parser {
669669
text: undefined,
670670
components: [],
671671
};
672-
do {
672+
// eslint-disable-next-line no-constant-condition
673+
while (true) {
673674
if (this.nextInputCodePointIndex >= this.text.length) {
674675
funcToken.text = name + '(' + this.text.substring(start);
675676

@@ -692,6 +693,6 @@ export class CSS3Parser {
692693
}
693694
// TODO: Else we won't advance
694695
}
695-
} while (true);
696+
}
696697
}
697698
}
Collapse file

‎packages/core/data/observable/index.spec.ts‎

Copy file name to clipboardExpand all lines: packages/core/data/observable/index.spec.ts
+103Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,107 @@ describe('Observable', () => {
9999
expect(callCount2).toBe(1);
100100
});
101101
});
102+
103+
describe('notify', () => {
104+
it('calls listeners in registration order with the event data', () => {
105+
const observable = new Observable();
106+
const calls: string[] = [];
107+
observable.on('test', (data) => calls.push('first:' + data.eventName));
108+
observable.on('test', (data) => calls.push('second:' + data.eventName));
109+
observable.notify({ eventName: 'test', object: observable });
110+
expect(calls).toEqual(['first:test', 'second:test']);
111+
});
112+
113+
it('binds thisArg as the callback context', () => {
114+
const observable = new Observable();
115+
const ctx = { name: 'ctx' };
116+
let receivedThis: any;
117+
let receivedData: any;
118+
observable.on(
119+
'test',
120+
function (this: any, data) {
121+
receivedThis = this;
122+
receivedData = data;
123+
},
124+
ctx,
125+
);
126+
observable.notify({ eventName: 'test', object: observable });
127+
expect(receivedThis).toBe(ctx);
128+
expect(receivedData.eventName).toBe('test');
129+
expect(receivedData.object).toBe(observable);
130+
});
131+
132+
it('allows a listener to remove itself during dispatch without affecting other listeners', () => {
133+
const observable = new Observable();
134+
const calls: string[] = [];
135+
const first = () => {
136+
calls.push('first');
137+
observable.off('test', first);
138+
};
139+
const second = () => calls.push('second');
140+
observable.on('test', first);
141+
observable.on('test', second);
142+
observable.notify({ eventName: 'test', object: observable });
143+
observable.notify({ eventName: 'test', object: observable });
144+
expect(calls).toEqual(['first', 'second', 'second']);
145+
});
146+
});
147+
148+
describe('hasListeners', () => {
149+
it('reflects listener registration and removal', () => {
150+
const observable = new Observable();
151+
expect(observable.hasListeners('test')).toBe(false);
152+
const handler = () => {
153+
// no-op
154+
};
155+
observable.on('test', handler);
156+
expect(observable.hasListeners('test')).toBe(true);
157+
observable.off('test', handler);
158+
expect(observable.hasListeners('test')).toBe(false);
159+
});
160+
});
161+
162+
describe('static (global) event handlers', () => {
163+
afterEach(() => {
164+
Observable.removeEventListener('globalTest');
165+
});
166+
167+
it('notifies global handlers registered on Observable for any instance', () => {
168+
const calls: string[] = [];
169+
Observable.addEventListener('globalTest', () => calls.push('global'));
170+
171+
const observable = new Observable();
172+
observable.on('globalTest', () => calls.push('instance'));
173+
observable.notify({ eventName: 'globalTest', object: observable });
174+
175+
expect(calls).toEqual(['instance', 'global']);
176+
});
177+
178+
it('notifies eventNameFirst global handlers before instance listeners', () => {
179+
const calls: string[] = [];
180+
Observable.addEventListener('globalTestFirst', () => calls.push('globalFirst'));
181+
182+
const observable = new Observable();
183+
observable.on('globalTest', () => calls.push('instance'));
184+
observable.notify({ eventName: 'globalTest', object: observable });
185+
186+
Observable.removeEventListener('globalTestFirst');
187+
188+
expect(calls).toEqual(['globalFirst', 'instance']);
189+
});
190+
191+
it('stops notifying after global handler removal', () => {
192+
let callCount = 0;
193+
const handler = () => callCount++;
194+
Observable.addEventListener('globalTest', handler);
195+
196+
const observable = new Observable();
197+
observable.notify({ eventName: 'globalTest', object: observable });
198+
expect(callCount).toBe(1);
199+
200+
Observable.removeEventListener('globalTest', handler);
201+
observable.notify({ eventName: 'globalTest', object: observable });
202+
expect(callCount).toBe(1);
203+
});
204+
});
102205
});

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.